commit 32a31f09090d3bc2f5df4194beb1fdcf9ee0409b Author: FinleyHsu <152379270+FinleyHsu@users.noreply.github.com> Date: Tue Jul 28 10:37:33 2026 +0800 feat: 初始化星元智灵人脸识别回收箱项目 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fb90cdf --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +.DS_Store +._codex_tmp/ +.idea/ +.gradle/ +*.iml +local.properties +build/ +sdk-demo/ +*.idsig +*.apk +*.ap_ +keystore/ +app/src/main/assets/face_models/face_recognizer.csta diff --git a/README.md b/README.md new file mode 100644 index 0000000..2d59a81 --- /dev/null +++ b/README.md @@ -0,0 +1,113 @@ +# 星元智灵 + +星元智灵是基于智投终端流程改造的竖屏 Android 投放终端应用。项目保留串口投口控制、称重回包、本地备份和服务器上报框架,并集成本地 SeetaFace6 人脸识别与人员录入流程。 +本项目为沃尔核材专用 APP,定位为安卓上位机应用。 + +## 主要功能 + +- 首页显示摄像头实时画面并默认进行本地人脸识别,也可通过刷卡或工号进入投放流程。 +- 设置页新增“人脸”Tab,可新增人员、录入或更新人脸、查看人员列表并删除人员。 +- 第二页填写本次投放信息:机台号、人员、生产批次。 +- 机台号支持手动输入,也支持小窗口表格选择;选择后自动联动人员和生产批次。 +- 第三页支持 1/2/3/4 投口模式,可选择单个投口或打开全部投口,页面展示本次机台号、人员、生产批次。 +- 投递页支持关门倒计时、手动关门、一次性延时关门。 +- 结果页仅显示本次投放垃圾类型和重量,不显示奖励。 +- 投口垃圾类型支持镀锡铜、铝合金、废铜、废电线、废铝、废锡和自定义。 +- 投放结果通过预留接口上报服务器,接口未定时使用本地队列和备份兜底。 +- 保留下位机原始称重小数;串口或下位机不可用时不会生成虚假称重结果。 + +## 工程结构 + +```text +app/src/main/ + AndroidManifest.xml Android 应用配置 + assets/www/ WebView 前端界面 + assets/face_models/ SeetaFace6 人脸模型 + assets/brand/ App 图标源图 + java/ 原生桥、串口、TCP 上报逻辑 + jniLibs/armeabi-v7a/ 串口 native 库 + res/mipmap-*/ Android launcher 图标 +build.sh 无 Gradle 手动构建脚本 +``` + +## 构建要求 + +- macOS +- Android SDK,包含: + - `platforms/android-28/android.jar` + - `build-tools/34.0.0` +- JDK 8 或兼容 `javac --release 8` 的 JDK +- Android SDK platform-tools,用于 `adb install` + +仓库服务限制单文件最大 50MB,因此约 98MB 的 +`face_recognizer.csta` 被保存为 `model-parts/` 下的四个分片。 +`build.sh` 会在编译前自动校验 SHA-256 并恢复完整模型,无需手动合并。 + +## 构建 APK + +```bash +bash build.sh +``` + +如果只需要恢复或校验人脸模型,可单独执行: + +```bash +bash scripts/restore-face-models.sh +``` + +构建产物: + +```text +build/out/xingyuan-zhiling-signed.apk +``` + +首次构建时,如果本地没有 debug keystore,脚本会自动生成: + +```text +keystore/zhitou-debug.keystore +``` + +该 keystore 不提交到 Git。 + +## 安装到终端 + +```bash +$HOME/Library/Android/sdk/platform-tools/adb install -r build/out/xingyuan-zhiling-signed.apk +$HOME/Library/Android/sdk/platform-tools/adb shell monkey -p com.xingyuan.zhiling -c android.intent.category.LAUNCHER 1 +``` + +## 串口说明 + +默认串口配置: + +```text +path: /dev/ttyS4 +baudRate: 9600 +``` + +如果首页提示串口故障,请检查串口占用情况以及下位机连接。旧智投 APP 与星元智灵同时运行时,可能会竞争 `/dev/ttyS4`。 + +## 接口说明 + +当前机台号、人员、生产批次接口仍为测试数据实现,内置 `WAL-A01`、`WAL-A02`、`WAL-B01`、`WAL-B02` 四台机台供流程测试。后续接入正式接口时,需要替换: + +- `app/src/main/java/com/zhitou/terminal/ZhitouBridge.java` 中的 `fetchWorkContext` +- 前端 `app/src/main/assets/www/app.js` 中的接口解析和兜底逻辑 + +投放结果上报预留: + +- `reportXingyuanDelivery` +- `reportDeliveryResult` +- 本地备份:`/sdcard/smart/cache/xingyuan_zhiling_delivery_backup.csv` +- 本地 JSON 备份:`/sdcard/smart/cache/xingyuan_zhiling_delivery_backup.json` +- 日志导出:`/sdcard/smart/logs/` + +## 版本 + +当前应用版本: + +```text +versionName: 1.2.18-wal-stability-fix +versionCode: 107 +package: com.xingyuan.zhiling +``` diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..0e0f113 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/assets/brand/xingyuan_launcher_source.png b/app/src/main/assets/brand/xingyuan_launcher_source.png new file mode 100644 index 0000000..7d7cb6b Binary files /dev/null and b/app/src/main/assets/brand/xingyuan_launcher_source.png differ diff --git a/app/src/main/assets/face_models/SEETAFACE6-LICENSE.txt b/app/src/main/assets/face_models/SEETAFACE6-LICENSE.txt new file mode 100644 index 0000000..892a565 --- /dev/null +++ b/app/src/main/assets/face_models/SEETAFACE6-LICENSE.txt @@ -0,0 +1,11 @@ +Copyright (c) 2019, SeetaTech, +Institute of Computing Technology, Chinese Academy of Sciences, Beijing, China +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/app/src/main/assets/face_models/face_detector.csta b/app/src/main/assets/face_models/face_detector.csta new file mode 100644 index 0000000..5f73a43 Binary files /dev/null and b/app/src/main/assets/face_models/face_detector.csta differ diff --git a/app/src/main/assets/face_models/face_landmarker_pts5.csta b/app/src/main/assets/face_models/face_landmarker_pts5.csta new file mode 100644 index 0000000..6bc1b40 Binary files /dev/null and b/app/src/main/assets/face_models/face_landmarker_pts5.csta differ diff --git a/app/src/main/assets/www/app.js b/app/src/main/assets/www/app.js new file mode 100644 index 0000000..0b0ca5a --- /dev/null +++ b/app/src/main/assets/www/app.js @@ -0,0 +1,5272 @@ +(function () { + "use strict"; + + if (!Element.prototype.matches) { + Element.prototype.matches = + Element.prototype.msMatchesSelector || + Element.prototype.webkitMatchesSelector; + } + + if (!Element.prototype.closest) { + Element.prototype.closest = function (selector) { + var node = this; + while (node && node.nodeType === 1) { + if (node.matches(selector)) return node; + node = node.parentElement || node.parentNode; + } + return null; + }; + } + + if (!String.prototype.repeat) { + String.prototype.repeat = function (count) { + var value = String(this); + var output = ""; + var i; + count = Number(count) || 0; + for (i = 0; i < count; i += 1) output += value; + return output; + }; + } + + var STORAGE_KEY = "xingyuan-zhiling-app:v1"; + var DEFAULT_REPORT_IP = "112.74.41.228"; + var LEGACY_REPORT_IP = ["218", "59", "175", "31"].join("."); + var DEFAULT_REPORT_PORT = 9956; + var DEFAULT_HTTP_BASE = "http://www.kinglive.com.cn/GreenLive-APP/"; + var DEFAULT_MEMBER_ID = "437306"; + var DEFAULT_PACKAGE_TYPE = "FinleyHsu"; + var STATUS_REPORT_INTERVAL_MS = 10 * 60 * 1000; + var WEIGHT_SETTLE_DELAY_MS = 500; + var WEIGHT_SAMPLE_INTERVAL_MS = 300; + var WEIGHT_SAMPLE_COUNT = 3; + var BASELINE_WEIGHT_TIMEOUT_MS = 1500; + var DELAY_CLOSE_SECONDS = 120; + var WORK_INFO_TIMEOUT_MS = 15 * 1000; + var STARTUP_CHECK_MAX_ATTEMPTS = 3; + var STARTUP_RESULT_HOLD_MS = 3000; + var MAX_PORT_COUNT = 4; + var RANGE_FULL_CONFIRM_CM = 65; + var RANGE_NORMAL_CONFIRM_CM = 75; + var RANGE_FULL_CONFIRM_COUNT = 3; + var RANGE_NORMAL_CONFIRM_COUNT = 2; + var GARBAGE_TYPES = { + tinCopper: { code: "1", zh: "镀锡铜", tw: "鍍錫銅", en: "Tinned copper" }, + aluminumAlloy: { code: "2", zh: "铝合金", tw: "鋁合金", en: "Aluminum alloy" }, + scrapCopper: { code: "3", zh: "废铜", tw: "廢銅", en: "Scrap copper" }, + scrapWire: { code: "4", zh: "废电线", tw: "廢電線", en: "Scrap wire" }, + scrapAluminum: { code: "5", zh: "废铝", tw: "廢鋁", en: "Scrap aluminum" }, + scrapTin: { code: "6", zh: "废锡", tw: "廢錫", en: "Scrap tin" }, + custom: { code: "1", zh: "自定义", tw: "自定義", en: "Custom" } + }; + + var I18N = { + "zh-CN": { + appName: "星元智灵", + subtitle: "智能投放终端", + homePrompt: "请点击下方按钮开始投放", + homeHint: "请选择本次投放的机台号、人员和生产批次", + admin: "管理员登录", + login: "开始投放", + available: "设备可用", + unavailable: "设备暂不可用", + serviceDown: "服务已断开", + maintenance: "设备维护中", + full: "设备已装满", + choosePort: "请选择投放口", + openAllPorts: "打开全部投口", + allPorts: "全部投口", + allPortsBlocked: "存在异常投口,无法打开全部投口", + allPortsOpened: "已打开全部投口", + openAllPortsFailed: "全部投口开门失败,请检查连接后重试", + delivery: "请投递物品", + closeDoor: "关闭投放门", + success: "投递成功", + backHome: "返回首页", + settingsTitle: "系统设置", + exitSettings: "退出设置", + tabDevice: "设备", + tabPorts: "投口", + tabCache: "缓存", + tabFace: "人脸", + tabSystem: "系统", + deviceParams: "设备参数", + deviceNo: "设备编号", + terminalImei: "终端号/IMEI", + serialPath: "串口路径", + baudRate: "波特率", + protocol: "协议", + deviceCheck: "设备检测", + networkTest: "网络测试", + getLowerInfo: "获取下位机信息", + restartSelfCheck: "重新启动自检", + firmwareVersion: "固件版本号", + lowerImei: "下位机IMEI号", + maintenanceMode: "维护模式", + maintenanceOnDesc: "维护模式已启用:投放和鉴权功能暂停,投放门保持打开。", + maintenanceOffDesc: "维护模式未启用,设备按正常流程运行。", + closeMaintenance: "关闭维护模式", + enableMaintenance: "启用维护模式", + deviceDisabledTitle: "设备禁用", + deviceDisabledOnDesc: "设备已禁用:首页显示设备维护中,登陆和投放不可用,仅允许打开清运门。", + deviceDisabledOffDesc: "设备禁用未启用,设备按正常流程运行。", + closeDeviceDisabled: "关闭设备禁用", + enableDeviceDisabled: "启用设备禁用", + openCollectionDoor: "打开清运门", + portCount: "投口数量", + enabledPorts: "启用投口", + portUnit: "投口", + portUnitPlural: "投口", + portTypeStatus: "投口类型与状态", + portSuffix: "号投口", + portWord: "投口", + portDisabledSuffix: "(未启用)", + garbageType: "垃圾类型", + customName: "自定义名称", + customTypeId: "自定义 type id", + currentWeightKg: "当前重量", + sensorSensitivityCm: "传感器感度 cm", + temperatureC: "温度 °C", + overflow: "满溢", + overflowHelp: "满溢后对应投口不可选择", + status: "状态", + normal: "正常", + save: "保存", + localBackupMode: "本地备份模式", + localBackupOnDesc: "本地备份已启用:投放完成后会写入 smart/cache 文件夹内的 CSV/JSON 备份文件。", + localBackupOffDesc: "本地备份未启用:投放数据仅按当前上报/缓存流程处理。", + localBackupClose: "本地备份:关闭", + localBackupOpen: "本地备份:启用", + localBackupStorageHint: "优先保存机身/外部存储 smart/cache 文件夹。", + backupPath: "备份路径", + generatedAfterEnable: "启用后自动生成", + localRecord: "本地记录", + recordUnit: "条", + recordUnitPlural: "条", + cacheRules: "离线缓存规则", + retainDays: "保留天数", + retryIntervalHours: "重试间隔小时", + retryNow: "立即补传", + cleanupExpired: "清理过期缓存", + pendingQueue: "待传队列", + noPendingData: "当前没有待传数据。", + languageSystem: "语言与系统", + language: "语言", + systemTools: "系统工具", + changePassword: "修改登录密码", + debugLog: "查看日志", + exportLogs: "导出日志", + logExported: "日志已导出", + logExportFailed: "日志导出失败", + enabled: "启用", + disabled: "禁用", + openSystemSettings: "系统设置", + openFileManager: "文件管理", + passwordCurrentPrompt: "请输入原登录密码", + passwordNextPrompt: "请输入新登录密码", + passwordConfirmPrompt: "请再次输入新登录密码", + savePassword: "保存密码", + nextStep: "下一步", + cancel: "取消", + clear: "清空", + backspace: "退格", + pass: "通过", + fail: "失败", + customEditable: "自定义 / 可编辑", + reading: "读取中...", + readFailed: "获取失败", + notEntered: "未输入", + password: "密码", + adminLoginDesc: "30 秒超时或输错超过 5 次返回首页。", + adminTimeout: "管理员登录超时", + passwordWrong: "密码错误", + adminTooManyErrors: "密码错误超过 5 次", + passwordPlaceholder: "请输入管理员密码", + showPassword: "显示密码", + hidePassword: "隐藏密码", + remainingTime: "剩余时间", + errorCount: "错误次数", + secondUnit: "秒", + submitLogin: "登录", + networkUnavailableQueue: "网络或服务器不可用,补传继续排队", + cacheRetryEmpty: "没有待补传数据", + cacheRetryPrefix: "已补传 ", + cacheRetrySuffix: " 条缓存", + cacheRetryRemainPrefix: ",", + cacheRetryRemainSuffix: " 条继续排队", + cacheRetryFail: "补传失败,数据继续排队", + cacheCleanupPrefix: "已清理 ", + cacheCleanupSuffix: " 条过期缓存", + localBackupEnabledToast: "本地备份已启用", + localBackupDisabledToast: "本地备份已关闭", + maintenanceEnabledToast: "维护模式已启用", + maintenanceDisabledToast: "维护模式已关闭", + deviceDisabledEnabledToast: "设备已禁用", + deviceDisabledDisabledToast: "设备已恢复正常", + debugLogEnabledToast: "查看日志已启用", + debugLogDisabledToast: "查看日志已禁用", + portSettingsSaved: "投口设置已保存", + passwordCurrentWrong: "原密码错误", + passwordRule: "新密码需为 4-12 位数字", + passwordMismatch: "两次输入不一致", + passwordChanged: "登录密码已修改", + networkState: "网络状态", + tcpReportParams: "TCP 上报参数", + networkTestOk: "网络测试通过", + networkTestFail: "网络测试失败", + readingLowerVersion: "正在读取下位机版本", + lowerVersionMissing: "未读取到下位机版本", + lowerVersionRequestFail: "下位机版本请求失败", + lowerVersionReceived: "已获取下位机版本", + readingLowerImei: "正在读取下位机IMEI", + lowerImeiMissing: "未读取到下位机IMEI", + lowerImeiRequestFail: "下位机IMEI请求失败", + lowerImeiReceived: "已获取下位机IMEI", + lowerInfoReceived: "已获取下位机信息", + readingLowerInfo: "正在读取下位机信息", + lowerInfoTimeout: "下位机信息读取超时", + enableDeviceDisabledFirst: "请先启用设备禁用", + collectionDoorCommandFail: "清运门开启指令发送失败", + collectionDoorCommandSent: "已发送清运门开启指令" + }, + "zh-TW": { + appName: "星元智靈", + subtitle: "智慧投放終端", + homePrompt: "請點擊下方按鈕開始投放", + homeHint: "請選擇本次投放的機台號、人員和生產批次", + admin: "管理員登入", + login: "開始投放", + available: "設備可用", + unavailable: "設備暫不可用", + serviceDown: "服務已斷開", + maintenance: "設備維護中", + full: "設備已裝滿", + choosePort: "請選擇投放口", + openAllPorts: "打開全部投口", + allPorts: "全部投口", + allPortsBlocked: "存在異常投口,無法打開全部投口", + allPortsOpened: "已打開全部投口", + openAllPortsFailed: "全部投口開門失敗,請檢查連線後重試", + delivery: "請投遞物品", + closeDoor: "關閉投放門", + success: "投遞成功", + backHome: "返回首頁", + settingsTitle: "系統設定", + exitSettings: "退出設定", + tabDevice: "設備", + tabPorts: "投口", + tabCache: "快取", + tabFace: "人臉", + tabSystem: "系統", + deviceParams: "設備參數", + deviceNo: "設備編號", + terminalImei: "終端號/IMEI", + serialPath: "串口路徑", + baudRate: "鮑率", + protocol: "協議", + deviceCheck: "設備檢測", + networkTest: "網路測試", + getLowerInfo: "取得下位機資訊", + restartSelfCheck: "重新啟動自檢", + firmwareVersion: "韌體版本號", + lowerImei: "下位機IMEI號", + maintenanceMode: "維護模式", + maintenanceOnDesc: "維護模式已啟用:投放和鑑權功能暫停,投放門保持打開。", + maintenanceOffDesc: "維護模式未啟用,設備按正常流程運行。", + closeMaintenance: "關閉維護模式", + enableMaintenance: "啟用維護模式", + deviceDisabledTitle: "設備停用", + deviceDisabledOnDesc: "設備已停用:首頁顯示設備維護中,登入和投放不可用,僅允許打開清運門。", + deviceDisabledOffDesc: "設備停用未啟用,設備按正常流程運行。", + closeDeviceDisabled: "關閉設備停用", + enableDeviceDisabled: "啟用設備停用", + openCollectionDoor: "打開清運門", + portCount: "投口數量", + enabledPorts: "啟用投口", + portUnit: "投口", + portUnitPlural: "投口", + portTypeStatus: "投口類型與狀態", + portSuffix: "號投口", + portWord: "投口", + portDisabledSuffix: "(未啟用)", + garbageType: "垃圾類型", + customName: "自訂名稱", + customTypeId: "自訂 type id", + currentWeightKg: "目前重量", + sensorSensitivityCm: "感測器感度 cm", + temperatureC: "溫度 °C", + overflow: "滿溢", + overflowHelp: "滿溢後對應投口不可選擇", + status: "狀態", + normal: "正常", + save: "儲存", + localBackupMode: "本地備份模式", + localBackupOnDesc: "本地備份已啟用:投放完成後會寫入 smart/cache 資料夾內的 CSV/JSON 備份檔。", + localBackupOffDesc: "本地備份未啟用:投放資料僅按目前上報/快取流程處理。", + localBackupClose: "本地備份:關閉", + localBackupOpen: "本地備份:啟用", + localBackupStorageHint: "優先儲存機身/外部儲存 smart/cache 資料夾。", + backupPath: "備份路徑", + generatedAfterEnable: "啟用後自動生成", + localRecord: "本地記錄", + recordUnit: "條", + recordUnitPlural: "條", + cacheRules: "離線快取規則", + retainDays: "保留天數", + retryIntervalHours: "重試間隔小時", + retryNow: "立即補傳", + cleanupExpired: "清理過期快取", + pendingQueue: "待傳佇列", + noPendingData: "目前沒有待傳資料。", + languageSystem: "語言與系統", + language: "語言", + systemTools: "系統工具", + changePassword: "修改登入密碼", + debugLog: "查看日誌", + exportLogs: "匯出日誌", + logExported: "日誌已匯出", + logExportFailed: "日誌匯出失敗", + enabled: "啟用", + disabled: "停用", + openSystemSettings: "系統設定", + openFileManager: "檔案管理", + passwordCurrentPrompt: "請輸入原登入密碼", + passwordNextPrompt: "請輸入新登入密碼", + passwordConfirmPrompt: "請再次輸入新登入密碼", + savePassword: "儲存密碼", + nextStep: "下一步", + cancel: "取消", + clear: "清空", + backspace: "退格", + pass: "通過", + fail: "失敗", + customEditable: "自訂 / 可編輯", + reading: "讀取中...", + readFailed: "取得失敗", + notEntered: "未輸入", + password: "密碼", + adminLoginDesc: "30 秒逾時或輸錯超過 5 次返回首頁。", + adminTimeout: "管理員登入逾時", + passwordWrong: "密碼錯誤", + adminTooManyErrors: "密碼錯誤超過 5 次", + passwordPlaceholder: "請輸入管理員密碼", + showPassword: "顯示密碼", + hidePassword: "隱藏密碼", + remainingTime: "剩餘時間", + errorCount: "錯誤次數", + secondUnit: "秒", + submitLogin: "登入", + networkUnavailableQueue: "網路或伺服器不可用,補傳繼續排隊", + cacheRetryEmpty: "沒有待補傳資料", + cacheRetryPrefix: "已補傳 ", + cacheRetrySuffix: " 條快取", + cacheRetryRemainPrefix: ",", + cacheRetryRemainSuffix: " 條繼續排隊", + cacheRetryFail: "補傳失敗,資料繼續排隊", + cacheCleanupPrefix: "已清理 ", + cacheCleanupSuffix: " 條過期快取", + localBackupEnabledToast: "本地備份已啟用", + localBackupDisabledToast: "本地備份已關閉", + maintenanceEnabledToast: "維護模式已啟用", + maintenanceDisabledToast: "維護模式已關閉", + deviceDisabledEnabledToast: "設備已停用", + deviceDisabledDisabledToast: "設備已恢復正常", + debugLogEnabledToast: "查看日誌已啟用", + debugLogDisabledToast: "查看日誌已停用", + portSettingsSaved: "投口設定已儲存", + passwordCurrentWrong: "原密碼錯誤", + passwordRule: "新密碼需為 4-12 位數字", + passwordMismatch: "兩次輸入不一致", + passwordChanged: "登入密碼已修改", + networkState: "網路狀態", + tcpReportParams: "TCP 上報參數", + networkTestOk: "網路測試通過", + networkTestFail: "網路測試失敗", + readingLowerVersion: "正在讀取下位機版本", + lowerVersionMissing: "未讀取到下位機版本", + lowerVersionRequestFail: "下位機版本請求失敗", + lowerVersionReceived: "已取得下位機版本", + readingLowerImei: "正在讀取下位機IMEI", + lowerImeiMissing: "未讀取到下位機IMEI", + lowerImeiRequestFail: "下位機IMEI請求失敗", + lowerImeiReceived: "已取得下位機IMEI", + lowerInfoReceived: "已取得下位機資訊", + readingLowerInfo: "正在讀取下位機資訊", + lowerInfoTimeout: "下位機資訊讀取逾時", + enableDeviceDisabledFirst: "請先啟用設備停用", + collectionDoorCommandFail: "清運門開啟指令發送失敗", + collectionDoorCommandSent: "已發送清運門開啟指令" + }, + en: { + appName: "Xingyuan Zhiling", + subtitle: "Smart Delivery Terminal", + homePrompt: "Tap the button below to start delivery", + homeHint: "Choose the machine number, operator, and production batch", + admin: "Admin Login", + login: "Start Delivery", + available: "Device ready", + unavailable: "Device unavailable", + serviceDown: "Service disconnected", + maintenance: "Under maintenance", + full: "Device is full", + choosePort: "Choose a chute", + openAllPorts: "Open All Chutes", + allPorts: "All Chutes", + allPortsBlocked: "Some chutes are unavailable. Cannot open all chutes.", + allPortsOpened: "All chutes opened", + openAllPortsFailed: "Failed to open all chutes. Check the connection and retry.", + delivery: "Place recyclables", + closeDoor: "Close door", + success: "Completed", + backHome: "Home", + settingsTitle: "System Settings", + exitSettings: "Exit Settings", + tabDevice: "Device", + tabPorts: "Chutes", + tabCache: "Cache", + tabFace: "Faces", + tabSystem: "System", + deviceParams: "Device Parameters", + deviceNo: "Device No.", + terminalImei: "Terminal No./IMEI", + serialPath: "Serial Path", + baudRate: "Baud Rate", + protocol: "Protocol", + deviceCheck: "Device Check", + networkTest: "Network Test", + getLowerInfo: "Get Controller Info", + restartSelfCheck: "Restart Self-check", + firmwareVersion: "Firmware Version", + lowerImei: "Controller IMEI", + maintenanceMode: "Maintenance Mode", + maintenanceOnDesc: "Maintenance mode is enabled: delivery and authentication are paused, and delivery doors remain open.", + maintenanceOffDesc: "Maintenance mode is disabled. The device is running normally.", + closeMaintenance: "Disable Maintenance", + enableMaintenance: "Enable Maintenance", + deviceDisabledTitle: "Device Disabled", + deviceDisabledOnDesc: "The device is disabled: the home screen shows maintenance mode, login and delivery are unavailable, and only the collection door can be opened.", + deviceDisabledOffDesc: "Device disable mode is off. The device is running normally.", + closeDeviceDisabled: "Enable Device", + enableDeviceDisabled: "Disable Device", + openCollectionDoor: "Open Collection Door", + portCount: "Chute Count", + enabledPorts: "Enabled Chutes", + portUnit: "chute", + portUnitPlural: "chutes", + portTypeStatus: "Chute Type and Status", + portSuffix: " chute", + portWord: "Chute", + portDisabledSuffix: "(Disabled)", + garbageType: "Material Type", + customName: "Custom Name", + customTypeId: "Custom type id", + currentWeightKg: "Current Weight", + sensorSensitivityCm: "Sensor Sensitivity cm", + temperatureC: "Temperature °C", + overflow: "Overflow", + overflowHelp: "This chute cannot be selected while overflow is on", + status: "Status", + normal: "Normal", + save: "Save", + localBackupMode: "Local Backup Mode", + localBackupOnDesc: "Local backup is enabled: delivery records are written to CSV/JSON backup files in the smart/cache folder.", + localBackupOffDesc: "Local backup is disabled: delivery data only follows the current upload/cache flow.", + localBackupClose: "Local Backup: Off", + localBackupOpen: "Local Backup: On", + localBackupStorageHint: "Prefer the smart/cache folder on internal or external storage.", + backupPath: "Backup Path", + generatedAfterEnable: "Generated after enabling", + localRecord: "Local Records", + recordUnit: "record", + recordUnitPlural: "records", + cacheRules: "Offline Cache Rules", + retainDays: "Retention Days", + retryIntervalHours: "Retry Interval Hours", + retryNow: "Retry Now", + cleanupExpired: "Clean Expired Cache", + pendingQueue: "Pending Queue", + noPendingData: "There is no pending data.", + languageSystem: "Language and System", + language: "Language", + systemTools: "System Tools", + changePassword: "Change Login Password", + debugLog: "Debug Log", + exportLogs: "Export Logs", + logExported: "Logs exported", + logExportFailed: "Log export failed", + enabled: "Enabled", + disabled: "Disabled", + openSystemSettings: "System Settings", + openFileManager: "File Manager", + passwordCurrentPrompt: "Enter current login password", + passwordNextPrompt: "Enter new login password", + passwordConfirmPrompt: "Enter new login password again", + savePassword: "Save Password", + nextStep: "Next", + cancel: "Cancel", + clear: "Clear", + backspace: "Backspace", + pass: "Pass", + fail: "Fail", + customEditable: "Custom / Editable", + reading: "Reading...", + readFailed: "Read failed", + notEntered: "Not entered", + password: "Password", + adminLoginDesc: "Returns home after 30 seconds or more than 5 failed attempts.", + adminTimeout: "Admin login timed out", + passwordWrong: "Incorrect password", + adminTooManyErrors: "More than 5 failed password attempts", + passwordPlaceholder: "Enter admin password", + showPassword: "Show", + hidePassword: "Hide", + remainingTime: "Time left", + errorCount: "Errors", + secondUnit: "s", + submitLogin: "Login", + networkUnavailableQueue: "Network or server unavailable. Upload remains queued.", + cacheRetryEmpty: "No pending data to retry", + cacheRetryPrefix: "Retried ", + cacheRetrySuffix: " cached item(s)", + cacheRetryRemainPrefix: ", ", + cacheRetryRemainSuffix: " still queued", + cacheRetryFail: "Retry failed; data remains queued", + cacheCleanupPrefix: "Cleaned ", + cacheCleanupSuffix: " expired cached item(s)", + localBackupEnabledToast: "Local backup enabled", + localBackupDisabledToast: "Local backup disabled", + maintenanceEnabledToast: "Maintenance mode enabled", + maintenanceDisabledToast: "Maintenance mode disabled", + deviceDisabledEnabledToast: "Device disabled", + deviceDisabledDisabledToast: "Device restored", + debugLogEnabledToast: "Debug log enabled", + debugLogDisabledToast: "Debug log disabled", + portSettingsSaved: "Chute settings saved", + passwordCurrentWrong: "Current password is incorrect", + passwordRule: "New password must be 4-12 digits", + passwordMismatch: "Passwords do not match", + passwordChanged: "Login password changed", + networkState: "Network Status", + tcpReportParams: "TCP Report Parameters", + networkTestOk: "Network test passed", + networkTestFail: "Network test failed", + readingLowerVersion: "Reading controller version", + lowerVersionMissing: "Controller version not received", + lowerVersionRequestFail: "Controller version request failed", + lowerVersionReceived: "Controller version received", + readingLowerImei: "Reading controller IMEI", + lowerImeiMissing: "Controller IMEI not received", + lowerImeiRequestFail: "Controller IMEI request failed", + lowerImeiReceived: "Controller IMEI received", + lowerInfoReceived: "Controller info received", + readingLowerInfo: "Reading controller info", + lowerInfoTimeout: "Controller info read timed out", + enableDeviceDisabledFirst: "Enable device disable mode first", + collectionDoorCommandFail: "Failed to send collection door open command", + collectionDoorCommandSent: "Collection door open command sent" + } + }; + + var app = document.getElementById("app"); + var toast = document.getElementById("toast"); + var timers = { + boot: null, + delivery: null, + success: null, + authFail: null, + admin: null, + toast: null, + queue: null, + status: null, + reader: null, + workInfoTimeout: null, + selectTimeout: null, + baselineWeight: null, + weightTimeout: null, + weightSample: null, + lowerImei: null, + lowerVersion: null, + lowerInfo: null, + settingsTelemetry: null, + faceSync: null + }; + var readerBuffer = ""; + var readerLastAt = 0; + var rangeStability = {}; + var workContextRequestId = 0; + var connectionCheckRequestId = 0; + var facePreviewLayoutQueued = false; + + var state = { + screen: "boot", + activeTab: "device", + config: defaultConfig(), + deviceStatus: defaultDeviceStatus(), + check: defaultCheck(), + auth: { status: "idle", message: "" }, + employeeLogin: { open: false, employeeNo: "", message: "" }, + faceSync: { syncing: false, lastAt: 0, count: 0, message: "尚未同步" }, + faceRecognition: { status: "starting", message: "正在启动人脸识别", enrolledCount: 0 }, + faceEnrollment: { employeeNo: "", name: "", selectedFaceId: "", capturing: false, message: "" }, + faceDirectory: [], + facePeople: [], + workInfo: defaultWorkInfo(), + adminLogin: { tries: 0, secondsLeft: 30, message: "", value: "", showPassword: false }, + passwordChange: { open: false, step: "current", current: "", next: "", confirm: "", message: "" }, + session: null, + delivery: null, + offlineQueue: [], + localBackups: [], + logs: [], + settingsDraft: null, + portSettingsPort: 0, + testResults: null, + lowerVersion: null, + lowerImei: null, + serialPorts: { loaded: false, loading: false, items: [], message: "" } + }; + + function defaultConfig() { + return { + language: "zh-CN", + appVersion: "1.2.18-wal-stability-fix", + adminPassword: "88888888", + portCount: 2, + debugLog: { + enabled: false + }, + guest: { + enabled: false + }, + localBackup: { + enabled: true, + path: "" + }, + device: { + deviceNo: "", + terminalId: "", + serialPath: "/dev/ttyS4", + baudRate: 9600, + protocol: "680068xx", + board: "RK3288" + }, + server: { + tcpHost: DEFAULT_REPORT_IP, + tcpPort: DEFAULT_REPORT_PORT, + httpBase: DEFAULT_HTTP_BASE, + upgradeEndpoint: "autoMachine/version.do", + defaultMemberId: DEFAULT_MEMBER_ID, + packageType: DEFAULT_PACKAGE_TYPE + }, + portTypes: { + 1: "tinCopper", + 2: "aluminumAlloy", + 3: "tinCopper", + 4: "aluminumAlloy" + }, + customPortTypes: { + 1: { name: "自定义1", code: "1" }, + 2: { name: "自定义2", code: "2" }, + 3: { name: "自定义3", code: "1" }, + 4: { name: "自定义4", code: "2" } + }, + cache: { + retainDays: 30, + retryIntervalHours: 1 + }, + face: { + autoSyncHours: 4, + matchThreshold: 0.75 + } + }; + } + + function defaultDeviceStatus() { + return { + serialOnline: false, + networkOnline: true, + tcpConnected: true, + httpReachable: true, + maintenance: false, + deviceDisabled: false, + collectionDoorClosed: true, + ports: { + 1: { overflow: false, temperature: 26, infraredBlocked: false, weight: 0, weightKnown: false, range: 150 }, + 2: { overflow: false, temperature: 25, infraredBlocked: false, weight: 0, weightKnown: false, range: 150 }, + 3: { overflow: false, temperature: 27, infraredBlocked: false, weight: 0, weightKnown: false, range: 150 }, + 4: { overflow: false, temperature: 26, infraredBlocked: false, weight: 0, weightKnown: false, range: 150 } + } + }; + } + + function defaultCheck() { + return { + attempt: 0, + serial: false, + network: false, + completed: false, + available: false, + skipped: false, + running: false + }; + } + + function defaultWorkInfo() { + return { + loading: false, + loaded: false, + error: "", + machineNo: "", + person: "", + batch: "", + pickerField: "", + options: [] + }; + } + + function deepMerge(base, incoming) { + if (!incoming || typeof incoming !== "object") return base; + Object.keys(incoming).forEach(function (key) { + if ( + incoming[key] && + typeof incoming[key] === "object" && + !Array.isArray(incoming[key]) && + base[key] && + typeof base[key] === "object" && + !Array.isArray(base[key]) + ) { + deepMerge(base[key], incoming[key]); + } else { + base[key] = incoming[key]; + } + }); + return base; + } + + function cloneData(value) { + return JSON.parse(JSON.stringify(value || {})); + } + + function isPlainObject(value) { + return Boolean(value && typeof value === "object" && !Array.isArray(value)); + } + + function firstArrayValue(source, keys) { + var i; + if (Array.isArray(source)) return source; + if (!isPlainObject(source)) return []; + for (i = 0; i < keys.length; i += 1) { + if (Array.isArray(source[keys[i]])) return source[keys[i]]; + } + return []; + } + + function sanitizeQueueItem(item) { + var output; + var createdAt; + if (!isPlainObject(item)) return null; + output = cloneData(item); + createdAt = Number(output.createdAt || Date.now()); + if (!isFinite(createdAt)) createdAt = Date.now(); + output.id = String(output.id || "Q" + createdAt); + output.kind = String(output.kind || "delivery"); + output.kindText = String(output.kindText || (output.kind === "delivery" ? "投递记录" : "缓存记录")); + output.createdAt = createdAt; + output.status = String(output.status || "queued"); + output.summary = String(output.summary || output.kindText); + if (!isPlainObject(output.payload)) output.payload = {}; + return output; + } + + function sanitizeOfflineQueue(items) { + return (Array.isArray(items) ? items : []) + .map(sanitizeQueueItem) + .filter(function (item) { + return Boolean(item); + }) + .slice(0, 500); + } + + function defaultPortStatus(port) { + var defaults = defaultDeviceStatus().ports; + return cloneData(defaults[port] || defaults[1]); + } + + function normalizeTemperatureValue(value, fallback) { + var number = Number(value); + if (!isFinite(number) || number <= 0 || number > 100) return fallback == null ? null : fallback; + return Number(number.toFixed(1)); + } + + function ensurePortStatus(port) { + var info; + var defaults = defaultPortStatus(port); + if (!state.deviceStatus.ports || typeof state.deviceStatus.ports !== "object") { + state.deviceStatus.ports = {}; + } + info = state.deviceStatus.ports[port]; + if (!info || typeof info !== "object") info = {}; + info = deepMerge(defaults, info); + info.overflow = Boolean(info.overflow); + info.infraredBlocked = Boolean(info.infraredBlocked); + info.temperature = normalizeTemperatureValue(info.temperature, defaults.temperature); + info.weight = isFinite(Number(info.weight)) ? Math.max(0, Number(info.weight)) : defaults.weight; + info.weightKnown = Boolean(info.weightKnown); + if (!info.weightKnown) info.weight = 0; + info.range = normalizeRangeValue(info.range) == null ? defaults.range : normalizeRangeValue(info.range); + state.deviceStatus.ports[port] = info; + return info; + } + + function createSettingsDraft() { + return { + config: { + portCount: state.config.portCount, + portTypes: cloneData(state.config.portTypes), + customPortTypes: cloneData(state.config.customPortTypes), + server: cloneData(state.config.server) + }, + deviceStatus: { + ports: cloneData(state.deviceStatus.ports) + } + }; + } + + function ensureSettingsDraft() { + if (!state.settingsDraft) state.settingsDraft = createSettingsDraft(); + return state.settingsDraft; + } + + function syncSettingsDraftPortTelemetry(port) { + var draftPort; + var livePort; + if (!state.settingsDraft || !state.settingsDraft.deviceStatus || !state.settingsDraft.deviceStatus.ports) return; + livePort = state.deviceStatus.ports && state.deviceStatus.ports[port]; + if (!livePort) return; + draftPort = state.settingsDraft.deviceStatus.ports[port] || defaultPortStatus(port); + draftPort.weight = livePort.weight; + draftPort.range = livePort.range; + draftPort.temperature = livePort.temperature; + state.settingsDraft.deviceStatus.ports[port] = draftPort; + } + + function loadPersisted() { + try { + var raw = localStorage.getItem(STORAGE_KEY); + if (!raw) return; + var saved = JSON.parse(raw); + state.config = deepMerge(defaultConfig(), saved.config || {}); + state.config.appVersion = defaultConfig().appVersion; + disableRemovedFeatures(); + migrateDefaultServer(saved.config || {}); + migrateSerialPath(saved.config || {}); + migrateTerminalImei(saved.config || {}); + migratePortConfig(); + state.deviceStatus = deepMerge(defaultDeviceStatus(), saved.deviceStatus || {}); + migratePortStatus(); + state.offlineQueue = sanitizeOfflineQueue(saved.offlineQueue); + state.localBackups = Array.isArray(saved.localBackups) ? saved.localBackups : []; + state.faceSync = deepMerge(state.faceSync, saved.faceSync || {}); + state.faceSync.syncing = false; + state.faceDirectory = Array.isArray(saved.faceDirectory) ? saved.faceDirectory : []; + } catch (error) { + showToast("本地配置读取失败,已使用默认配置"); + } + } + + function migrateDefaultServer(savedConfig) { + var savedServer = savedConfig.server || {}; + var savedHost = String(savedServer.tcpHost || "").trim(); + var savedHttpBase = String(savedServer.httpBase || "").trim(); + if (!savedHost || savedHost === "20.2.128.145" || savedHost === LEGACY_REPORT_IP) { + state.config.server.tcpHost = DEFAULT_REPORT_IP; + } + if (!savedServer.tcpPort || Number(savedServer.tcpPort) === 9878) { + state.config.server.tcpPort = DEFAULT_REPORT_PORT; + } + if (!savedHttpBase || /20\.2\.128\.145|112\.74\.41\.228/.test(savedHttpBase)) { + state.config.server.httpBase = DEFAULT_HTTP_BASE; + } + if (!savedServer.defaultMemberId) { + state.config.server.defaultMemberId = DEFAULT_MEMBER_ID; + } + if (!savedServer.packageType || savedServer.packageType === "Five_NEW_AI_SJZ" || savedServer.packageType === "Five_NEW_AI") { + state.config.server.packageType = DEFAULT_PACKAGE_TYPE; + } + } + + function migrateSerialPath(savedConfig) { + var savedDevice = savedConfig.device || {}; + var savedPath = String(savedDevice.serialPath || "").trim(); + // 旧版本曾默认写入 S1;升级后自动迁回下位机实际连接的 S4。 + if (!savedPath || savedPath === "/dev/ttyS1") { + state.config.device.serialPath = "/dev/ttyS4"; + } + } + + function migrateTerminalImei(savedConfig) { + var savedDevice = savedConfig.device || {}; + var terminalId = String(savedDevice.terminalId || "").trim(); + if (/^\d{10,20}$/.test(terminalId)) { + state.config.device.terminalId = terminalId; + state.lowerImei = terminalId; + return; + } + state.config.device.terminalId = ""; + state.lowerImei = null; + } + + function migratePortConfig() { + state.config.portCount = clampPortCount(state.config.portCount || 2); + if (!state.config.customPortTypes) state.config.customPortTypes = {}; + [1, 2, 3, 4].forEach(function (port) { + if (!GARBAGE_TYPES[state.config.portTypes[port]]) { + state.config.portTypes[port] = port % 2 === 0 ? "aluminumAlloy" : "tinCopper"; + } + customType(port); + }); + } + + function disableRemovedFeatures() { + if (!state.config.guest) state.config.guest = { enabled: false }; + state.config.guest.enabled = false; + if (!state.config.face) state.config.face = {}; + state.config.face.matchThreshold = Math.max(0.50, Math.min(0.95, + Number(state.config.face.matchThreshold || 0.75))); + // 清除旧版本持久化的模拟配置,升级后不能继续绕过真实串口和身份校验。 + delete state.config.simulation; + delete state.config.capture; + } + + function migratePortStatus() { + state.deviceStatus.deviceDisabled = Boolean(state.deviceStatus.deviceDisabled); + if (!state.deviceStatus.deviceDisabled) { + state.deviceStatus.collectionDoorClosed = true; + } + [1, 2, 3, 4].forEach(function (port) { + ensurePortStatus(port); + resetRangeStability(port); + }); + } + + function savePersisted() { + try { + localStorage.setItem( + STORAGE_KEY, + JSON.stringify({ + config: state.config, + deviceStatus: state.deviceStatus, + offlineQueue: state.offlineQueue, + localBackups: state.localBackups, + faceSync: state.faceSync, + faceDirectory: state.faceDirectory + }) + ); + configureNativeReporter(); + } catch (error) { + showToast("本地配置保存失败"); + } + } + + function isAdminPasswordValid(password) { + var value = String(password || ""); + return value === String(state.config.adminPassword); + } + + function t(key) { + var lang = state.config.language || "zh-CN"; + return (I18N[lang] && I18N[lang][key]) || I18N["zh-CN"][key] || key; + } + + function isEnglish() { + return (state.config.language || "zh-CN") === "en"; + } + + function portDisplayName(port) { + return isEnglish() ? t("portWord") + " " + port : port + " " + t("portSuffix"); + } + + function portCountLabel(count) { + return isEnglish() ? count + " " + (Number(count) === 1 ? t("portUnit") : t("portUnitPlural")) : count + " " + t("portUnit"); + } + + function recordCountLabel(count) { + return isEnglish() ? count + " " + (Number(count) === 1 ? t("recordUnit") : t("recordUnitPlural")) : count + " " + t("recordUnit"); + } + + function localizedReadStatus(value) { + var text = String(value == null ? "" : value); + if (text === "读取中..." || text === "讀取中..." || text === "Reading...") return t("reading"); + if (text === "获取失败" || text === "取得失敗" || text === "Read failed") return t("readFailed"); + return text; + } + + function localizedList(items) { + return items.join(isEnglish() ? ", " : "、"); + } + + function escapeHtml(value) { + return String(value == null ? "" : value) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); + } + + function attr(value) { + return escapeHtml(value); + } + + function checked(value) { + return value ? " checked" : ""; + } + + function selected(value, current) { + return String(value) === String(current) ? " selected" : ""; + } + + function money(value) { + return Number(value || 0).toFixed(2); + } + + function numericMoney(value) { + var cleaned = String(value == null ? "" : value).replace(/[^0-9.\-]/g, ""); + var parsed = Number(cleaned); + return isFinite(parsed) ? parsed : 0; + } + + function plainMoney(value) { + return money(numericMoney(value)).replace(/^[^\d]+/, ""); + } + + function kg(value) { + return Number(value || 0).toFixed(2) + " kg"; + } + + function kgCompact(value) { + return Number(value || 0).toFixed(2) + "kg"; + } + + function cleanWeightNumber(value) { + var number = Number(value); + return isFinite(number) ? Number(number.toFixed(6)) : 0; + } + + function nowText(timestamp) { + var d = new Date(timestamp || Date.now()); + var pad = function (n) { + return n < 10 ? "0" + n : String(n); + }; + return ( + d.getFullYear() + + "-" + + pad(d.getMonth() + 1) + + "-" + + pad(d.getDate()) + + " " + + pad(d.getHours()) + + ":" + + pad(d.getMinutes()) + ); + } + + function maskAccount(account) { + var text = String(account || ""); + if (text.length <= 4) return text; + return text.slice(0, 2) + "*".repeat(Math.max(3, text.length - 4)) + text.slice(-2); + } + + function maskPin(value) { + var text = String(value || ""); + return text ? "*".repeat(text.length) : t("notEntered"); + } + + function isLogEnabled() { + return Boolean(state.config.debugLog && state.config.debugLog.enabled); + } + + function logTimeText(timestamp) { + var d = new Date(timestamp || Date.now()); + var pad = function (n) { + return n < 10 ? "0" + n : String(n); + }; + return pad(d.getHours()) + ":" + pad(d.getMinutes()) + ":" + pad(d.getSeconds()); + } + + function addLog(kind, message, detail) { + message = String(message || "").trim(); + if (!message) return; + state.logs.unshift({ + at: Date.now(), + kind: String(kind || "窗口"), + message: message, + detail: detail == null ? "" : String(detail) + }); + if (state.logs.length > 80) state.logs.length = 80; + if (isLogEnabled() && state.screen !== "home") render(); + } + + function renderLogWindow() { + if (!isLogEnabled()) return ""; + var entries = state.logs.slice(0, 10); + return ( + '
' + + '
窗口日志输出传输日志
' + + '
' + + (entries.length + ? entries.map(function (item) { + return ( + '
' + + '' + + escapeHtml(logTimeText(item.at)) + + "" + + '' + + escapeHtml(item.kind) + + "" + + '' + + escapeHtml(item.message) + + (item.detail ? " / " + escapeHtml(item.detail) : "") + + "" + + "
" + ); + }).join("") + : '
等待日志输出
') + + "
" + + "
" + ); + } + + function showToast(message) { + addLog("窗口", message); + window.clearTimeout(timers.toast); + toast.textContent = message; + toast.classList.add("show"); + timers.toast = window.setTimeout(function () { + toast.classList.remove("show"); + }, 2200); + } + + function setPath(root, path, value) { + var parts = path.split("."); + var target = root; + for (var i = 0; i < parts.length - 1; i += 1) { + if (!target[parts[i]]) target[parts[i]] = {}; + target = target[parts[i]]; + } + target[parts[parts.length - 1]] = value; + } + + function getPath(root, path) { + return path.split(".").reduce(function (obj, key) { + return obj && obj[key]; + }, root); + } + + function clampPortCount(value) { + var count = Number(value); + if (!isFinite(count)) count = 2; + return Math.min(MAX_PORT_COUNT, Math.max(1, Math.round(count))); + } + + function allPhysicalPorts() { + var ports = []; + var i; + for (i = 1; i <= MAX_PORT_COUNT; i += 1) ports.push(i); + return ports; + } + + function enabledPorts() { + var count = clampPortCount(state.config.portCount || 2); + var ports = []; + for (var i = 1; i <= count; i += 1) ports.push(i); + return ports; + } + + function customType(port) { + if (!state.config.customPortTypes) state.config.customPortTypes = {}; + if (!state.config.customPortTypes[port]) state.config.customPortTypes[port] = { name: "自定义" + port, code: String(port % 2 === 0 ? 2 : 1) }; + if (String(state.config.customPortTypes[port].code || "") === "18") { + state.config.customPortTypes[port].code = String(port % 2 === 0 ? 2 : 1); + } + return state.config.customPortTypes[port]; + } + + function draftCustomType(port) { + var draft = ensureSettingsDraft(); + if (!draft.config.customPortTypes) draft.config.customPortTypes = {}; + if (!draft.config.customPortTypes[port]) draft.config.customPortTypes[port] = { name: "自定义" + port, code: String(port % 2 === 0 ? 2 : 1) }; + if (String(draft.config.customPortTypes[port].code || "") === "18") { + draft.config.customPortTypes[port].code = String(port % 2 === 0 ? 2 : 1); + } + return draft.config.customPortTypes[port]; + } + + function garbageName(key, port) { + if (key === "custom") return customType(port).name || "自定义"; + var type = GARBAGE_TYPES[key] || GARBAGE_TYPES.tinCopper; + var lang = state.config.language === "en" ? "en" : state.config.language === "zh-TW" ? "tw" : "zh"; + return type[lang]; + } + + function garbageCode(key, port) { + if (key === "custom") return String(customType(port).code || "1"); + return (GARBAGE_TYPES[key] || GARBAGE_TYPES.tinCopper).code; + } + + function deliveryGarbageName(key, port) { + if (key === "allPorts") return t("allPorts"); + return garbageName(key, port); + } + + function deliveryGarbageCode(key, port) { + if (key === "allPorts") return "0"; + return garbageCode(key, port); + } + + function numericGarbageCode(key, port) { + return Number(garbageCode(key, port)); + } + + function typeOptions(current) { + return Object.keys(GARBAGE_TYPES) + .map(function (key) { + var label = key === "custom" ? t("customEditable") : garbageName(key) + " / " + garbageCode(key); + return '"; + }) + .join(""); + } + + function workOptions() { + var info = state.workInfo || {}; + return Array.isArray(info.options) ? info.options : []; + } + + function machineOptions() { + return workOptions().map(function (item) { + return item.machineNo; + }); + } + + function selectedMachine() { + var machineNo = String((state.workInfo && state.workInfo.machineNo) || "").trim(); + var options = workOptions(); + var i; + for (i = 0; i < options.length; i += 1) { + if (String(options[i].machineNo || "") === machineNo) return options[i]; + } + return null; + } + + function personOptions() { + var machine = selectedMachine(); + if (machine && Array.isArray(machine.persons)) return machine.persons; + return uniqueWorkValues("persons"); + } + + function batchOptions() { + var machine = selectedMachine(); + if (machine && Array.isArray(machine.batches)) return machine.batches; + return uniqueWorkValues("batches"); + } + + function uniqueWorkValues(key) { + var seen = {}; + var output = []; + workOptions().forEach(function (item) { + (item[key] || []).forEach(function (value) { + value = String(value || "").trim(); + if (value && !seen[value]) { + seen[value] = true; + output.push(value); + } + }); + }); + return output; + } + + function uniqueTextList(values) { + var seen = {}; + var output = []; + (Array.isArray(values) ? values : []).forEach(function (value) { + value = cleanWorkText(value, 48); + if (value && !seen[value]) { + seen[value] = true; + output.push(value); + } + }); + return output; + } + + function cleanWorkText(value, maxLength) { + var text = String(value == null ? "" : value).replace(/\s+/g, " ").trim(); + var limit = Math.max(1, Number(maxLength || 48)); + return text.length > limit ? text.slice(0, limit) : text; + } + + function syncWorkInfoForMachine(force) { + var info = state.workInfo || defaultWorkInfo(); + var machine = selectedMachine(); + var persons; + var batches; + if (!machine) return false; + persons = Array.isArray(machine.persons) ? machine.persons : []; + batches = Array.isArray(machine.batches) ? machine.batches : []; + if (persons.length && (force ? persons.indexOf(info.person) < 0 : !info.person)) info.person = persons[0]; + if (batches.length && (force ? batches.indexOf(info.batch) < 0 : !info.batch)) info.batch = batches[0]; + state.workInfo = info; + return true; + } + + function normalizeWorkContext(raw) { + var list = firstArrayValue(raw, ["items", "data", "list", "rows", "machines", "records"]); + var byMachine = {}; + var order = []; + list.forEach(function (item) { + var persons; + var batches; + var machineNo; + var target; + if (!isPlainObject(item)) return; + persons = item.persons || item.people || item.operators || item.operator || item.staff || item.person || item.personName || item.userName || []; + batches = item.batches || item.productionBatches || item.productionBatch || item.batchNo || item.batch || []; + if (!Array.isArray(persons)) persons = String(persons || "").split(/[,,]/); + if (!Array.isArray(batches)) batches = String(batches || "").split(/[,,]/); + machineNo = cleanWorkText(item.machineNo || item.machine || item.machineId || item.machineCode || item.deviceNo || item.no, 48); + if (!machineNo) return; + if (!byMachine[machineNo]) { + byMachine[machineNo] = { machineNo: machineNo, persons: [], batches: [] }; + order.push(machineNo); + } + target = byMachine[machineNo]; + target.persons = uniqueTextList(target.persons.concat(persons)); + target.batches = uniqueTextList(target.batches.concat(batches)); + }); + var normalized = order.map(function (machineNo) { + return byMachine[machineNo]; + }); + return normalized; + } + + function sessionWorkInfo() { + return (state.session && state.session.workInfo) || { + machineNo: "", + person: "", + employeeName: "", + employeeNo: "", + identifiedAt: 0, + batch: "" + }; + } + + function sensorHeight(port) { + var info = ensurePortStatus(port); + var value = normalizeRangeValue(info.range); + return value == null ? 150 : value; + } + + function draftSensorHeight(port) { + var draft = ensureSettingsDraft(); + var info = (draft.deviceStatus.ports && draft.deviceStatus.ports[port]) || {}; + var value = normalizeRangeValue(info.range); + return value == null ? 150 : value; + } + + function resetRangeStability(port) { + rangeStability[port] = { full: 0, normal: 0 }; + } + + function normalizeRangeValue(value) { + var number = Number(value); + if (!isFinite(number) || number <= 0) return null; + return Math.max(1, Math.min(255, Math.round(number))); + } + + function applyRangeReading(port, value) { + var info = state.deviceStatus.ports[port]; + var range = normalizeRangeValue(value); + var tracker; + if (!info || range == null) return false; + info.range = range; + tracker = rangeStability[port] || { full: 0, normal: 0 }; + if (range <= RANGE_FULL_CONFIRM_CM) { + tracker.full += 1; + tracker.normal = 0; + if (tracker.full >= RANGE_FULL_CONFIRM_COUNT) info.overflow = true; + } else if (range >= RANGE_NORMAL_CONFIRM_CM) { + tracker.normal += 1; + tracker.full = 0; + if (tracker.normal >= RANGE_NORMAL_CONFIRM_COUNT) info.overflow = false; + } else { + tracker.full = 0; + tracker.normal = 0; + } + rangeStability[port] = tracker; + return true; + } + + function portTemperature(port) { + var info = ensurePortStatus(port); + return normalizeTemperatureValue(info.temperature, defaultPortStatus(port).temperature); + } + + function portBlockReason(port) { + if (state.deviceStatus.maintenance) return t("maintenanceMode"); + if (state.deviceStatus.deviceDisabled) return t("maintenance"); + if (ensurePortStatus(port).overflow) return t("full"); + return ""; + } + + function portStatusText(port) { + var reason = portBlockReason(port); + return reason || t("normal"); + } + + function portStatusTextForInfo(info) { + return info && info.overflow ? t("full") : t("normal"); + } + + function isPortBlocked(port) { + return Boolean(portBlockReason(port)); + } + + function topbar(subtitle, rightExtra) { + var actions = rightExtra || ""; + return ( + '
' + + '
' + + '
' + + '

' + + escapeHtml(t("appName")) + + '

' + + escapeHtml(subtitle || t("subtitle")) + + "

" + + "
" + + (actions ? '
' + actions + "
" : "") + + "
" + ); + } + + function statusPill(label, ok, badLabel) { + return '' + escapeHtml(ok ? label : badLabel) + ""; + } + + function render() { + var html = ""; + if (state.screen === "boot") html = renderBoot(); + if (state.screen === "home") html = renderHome(); + if (state.screen === "auth") html = renderAuth(); + if (state.screen === "workInfo") html = renderWorkInfo(); + if (state.screen === "select") html = renderSelect(); + if (state.screen === "delivery") html = renderDelivery(); + if (state.screen === "success") html = renderSuccess(); + if (state.screen === "adminLogin") html = renderAdminLogin(); + if (state.screen === "settings") html = renderSettings(); + app.innerHTML = html + renderLogWindow(); + var bridge = nativeBridge(); + var facePreviewActive = isFacePreviewActive(); + if (bridge && typeof bridge.setFaceRecognitionActive === "function") { + try { + bridge.setFaceRecognitionActive(JSON.stringify({ + active: facePreviewActive + })); + } catch (error) { + addLog("人脸", "切换摄像头状态失败", error.message || error); + } + } + syncFacePreviewLayout(facePreviewActive); + } + + function isFacePreviewActive() { + // 工号弹窗显示时先隐藏原生 SurfaceView,否则摄像头层会盖住 WebView 中央弹窗。 + return (state.screen === "home" && !(state.employeeLogin && state.employeeLogin.open)) || Boolean( + state.screen === "settings" && + state.activeTab === "face" && + state.faceEnrollment && + state.faceEnrollment.capturing + ); + } + + function syncFacePreviewLayout(visible) { + var bridge = nativeBridge(); + if (!bridge || typeof bridge.setFacePreviewLayout !== "function") return; + if (!visible) { + facePreviewLayoutQueued = false; + bridge.setFacePreviewLayout(JSON.stringify({ visible: false })); + return; + } + if (facePreviewLayoutQueued) return; + facePreviewLayoutQueued = true; + window.requestAnimationFrame(function () { + window.requestAnimationFrame(function () { + facePreviewLayoutQueued = false; + var host = document.getElementById("face-camera-preview"); + if (!host) { + bridge.setFacePreviewLayout(JSON.stringify({ visible: false })); + return; + } + if (host.classList.contains("home-camera-preview")) { + host.style.height = Math.round(host.getBoundingClientRect().width * 0.75) + "px"; + } + var rect = host.getBoundingClientRect(); + var scale = Number(window.devicePixelRatio || 1); + var inset = 4; + var visibleLeft = Math.max(rect.left + inset, 0); + var visibleTop = Math.max(rect.top + inset, 0); + var visibleRight = Math.min(rect.right - inset, window.innerWidth); + var visibleBottom = Math.min(rect.bottom - inset, window.innerHeight); + if (visibleRight <= visibleLeft || visibleBottom <= visibleTop) { + bridge.setFacePreviewLayout(JSON.stringify({ visible: false })); + return; + } + bridge.setFacePreviewLayout(JSON.stringify({ + visible: true, + left: Math.round(visibleLeft * scale), + top: Math.round(visibleTop * scale), + width: Math.max(1, Math.round((visibleRight - visibleLeft) * scale)), + height: Math.max(1, Math.round((visibleBottom - visibleTop) * scale)) + })); + }); + }); + } + + function renderBoot() { + var serialState = state.check.serial ? "ok" : state.check.completed ? "bad" : ""; + var networkState = state.check.network ? "ok" : state.check.completed ? "bad" : ""; + var bootDesc = state.check.completed + ? state.check.available + ? "串口和网络检测均已通过,检测结果显示 3 秒后自动进入首页。" + : "启动检测未通过,可以重新检测或点击“进入首页”。" + : "正在检查串口和 TCP/HTTP 网络状态,串口和网络均通过后才会自动进入首页。"; + return ( + '
' + + '
' + + '
' + + '

星元智灵启动自检

' + + '

' + + escapeHtml(bootDesc) + + "

" + + '
' + + checkRow("串口通讯", "SerialPort " + state.config.device.serialPath + " / " + state.config.device.baudRate, serialState, state.check.serial ? "已连接" : state.check.completed ? "未连接" : "检测中") + + checkRow("网络服务", state.config.server.tcpHost + ":" + state.config.server.tcpPort, networkState, state.check.network ? "已连接" : state.check.completed ? "未连接" : "检测中") + + "
" + + '

连接次数:' + + state.check.attempt + + " / " + STARTUP_CHECK_MAX_ATTEMPTS + "

" + + '
' + + (state.check.completed && state.check.available + ? '' + : '') + + (state.check.completed && !state.check.available + ? '' + : "") + + "
" + + "
" + + "
" + + "
" + ); + } + + function checkRow(name, meta, stateClass, text) { + var cls = stateClass ? " " + stateClass : ""; + return ( + '
' + + '
' + + escapeHtml(name) + + '
' + + escapeHtml(meta) + + "
" + + '
' + + escapeHtml(text) + + "
" + + "
" + ); + } + + function renderHome() { + var reason = homeBlockReason(); + var faceFeedback = homeFaceFeedback(); + return ( + '
' + + topbar(t("subtitle")) + + '
' + + '
' + + '
' + + '

' + + escapeHtml("请正对摄像头进行人脸识别") + + "

" + + '

' + + escapeHtml("人脸识别失败可选择工号进行登陆投放") + + "

" + + '
正在连接 USB 摄像头画面…
' + + (faceFeedback ? '
' + escapeHtml(faceFeedback) + '
' : '') + + '
' + + escapeHtml(reason || t("available")) + + "
" + + "
" + + renderHomeActions(reason) + + '" + + "
" + + renderEmployeeLoginDialog() + + "
" + + "
" + ); + } + + function homeFaceFeedback() { + var face = state.faceRecognition || {}; + var status = String(face.status || ""); + if (["face_error", "no_match", "error", "camera_error", "permission", "searching"].indexOf(status) < 0) { + return ""; + } + return String(face.message || "人脸识别失败"); + } + + function renderHomeActions(reason) { + var serialBlocked = !state.deviceStatus.serialOnline; + return ( + '
' + + '" + + "
" + ); + } + + function renderEmployeeLoginDialog() { + var login = state.employeeLogin || { open: false, employeeNo: "", message: "" }; + var people = employeeLoginPeople(); + var serialBlocked = !state.deviceStatus.serialOnline; + if (!login.open) return ""; + var choices = people.length + ? people.map(function (person) { + var active = String(login.employeeNo || "") === person.employeeNo; + return '"; + }).join("") + : '
暂无人员,请先到“设置 → 人脸”新增人员。
'; + return ( + '
' + + '
" + ); + } + + function employeeLoginPeople() { + var seen = {}; + return (Array.isArray(state.facePeople) && state.facePeople.length ? state.facePeople : state.faceDirectory || []) + .map(function (person) { + return { + employeeNo: String(person && person.employeeNo || "").trim(), + name: String(person && person.name || "").trim() + }; + }) + .filter(function (person) { + if (!person.employeeNo || seen[person.employeeNo]) return false; + seen[person.employeeNo] = true; + return true; + }) + .sort(function (first, second) { + return first.employeeNo.localeCompare(second.employeeNo); + }); + } + + function homeBlockReason() { + if (state.deviceStatus.maintenance || state.deviceStatus.deviceDisabled) return t("maintenance"); + if (!state.deviceStatus.collectionDoorClosed) return "收运门未关闭"; + if (!state.deviceStatus.serialOnline) return "串口故障:请检查串口占用情况以及下位机连接"; + if (!state.deviceStatus.networkOnline) return "网络故障"; + if (!state.deviceStatus.tcpConnected) return "服务器断开"; + if (state.check.completed && !state.check.available) return t("unavailable"); + return ""; + } + + function renderAuth() { + return ( + '
' + + topbar("身份鉴权") + + '
' + + '

读取用户身份

' + + '

二维码扫码器和 IC 卡读取结果会按协议上传。

' + + '
' + + '

' + + (state.auth.status === "reading" ? "正在请求服务器鉴权" : state.auth.status === "failed" ? "鉴权失败" : "准备鉴权") + + "

" + + '

' + + escapeHtml(state.auth.message || "请稍候,正在建立用户会话。") + + "

" + + '
' + + '" + + "
" + + "
" + + "
" + + "
" + ); + } + + function renderWorkInfo() { + var info = state.workInfo || defaultWorkInfo(); + var user = (state.session && state.session.user) || {}; + return ( + '
' + + topbar("投放信息") + + '
' + + '

信息核对

' + + '

请核对员工信息,并模糊输入或从列表选择机台号。15 秒未完成将返回首页。

' + + '
员工姓名' + + escapeHtml(user.name || "-") + + '
工号' + + escapeHtml(user.employeeNo || user.account || "-") + + '
识别时间' + + escapeHtml(user.identifiedAtText || "-") + + "
" + + (info.loading + ? '

正在获取投放信息

请稍候。

' + : renderWorkInfoForm(info)) + + "
" + + (!info.loading ? renderWorkPicker(info) : "") + + "
" + ); + } + + function renderWorkInfoForm(info) { + return ( + '
' + + (info.error ? '

' + escapeHtml(info.error) + "

" : "") + + pickerInput("机台号", "machineNo", info.machineNo, "请输入或选择机台号") + + '
' + + '' + + '' + + '' + + "
" + + "
" + ); + } + + function pickerInput(label, field, value, placeholder) { + return ( + '
' + ); + } + + function workInput(label, field, value, options, placeholder) { + var listId = "work-" + field + "-options"; + return ( + '
' + + '' + + options.map(function (item) { + return ''; + }).join("") + + "
" + ); + } + + function workPickerTitle(field) { + if (field === "person") return "选择人员"; + if (field === "batch") return "选择生产批次"; + return "选择机台号"; + } + + function workPickerOptions(field) { + if (field === "person") return personOptions(); + if (field === "batch") return batchOptions(); + return machineOptions(); + } + + function renderWorkPicker(info) { + var field = info.pickerField || ""; + if (!field) return ""; + var options = workPickerOptions(field); + var rows = []; + var i; + for (i = 0; i < options.length; i += 3) { + rows.push(options.slice(i, i + 3)); + } + return ( + '
' + + '
" + ); + } + + function renderSelect() { + var work = sessionWorkInfo(); + var ports = enabledPorts(); + return ( + '
' + + topbar("投口选择") + + '
' + + '

' + + escapeHtml(t("choosePort")) + + "

" + + '

请在 15 秒内选择投口。

' + + renderWorkSummary(work) + + '
' + + ports.map(renderPortCard).join("") + + "
" + + '
' + + '" + + '" + + "
" + + "
" + + "
" + ); + } + + function renderWorkSummary(work) { + work = work || {}; + return ( + '
' + + '
机台号' + + escapeHtml(work.machineNo || "-") + + "
" + + '
员工姓名' + + escapeHtml(work.employeeName || work.person || "-") + + "
" + + '
工号' + + escapeHtml(work.employeeNo || "-") + + "
" + + "
" + ); + } + + function renderPortCard(port) { + var typeKey = state.config.portTypes[port] || "other"; + var blocked = isPortBlocked(port); + return ( + '" + ); + } + + function renderDelivery() { + var d = state.delivery || {}; + var phase = deliveryPhaseText(); + return ( + '
' + + topbar("投递中") + + '
' + + '
' + + '
' + + escapeHtml(phase) + + "
" + + '
' + + (d.secondsLeft == null ? 30 : d.secondsLeft) + + "
" + + '
' + + escapeHtml(deliverySafetyText()) + + "
" + + '
' + + '" + + '" + + '" + + "
" + + "
" + + "
" + + "
" + ); + } + + function deliveryPhaseText() { + var d = state.delivery || {}; + if (d.baselineWaiting) return d.allPorts ? "正在读取全部投口的投放前重量" : "正在读取投放前重量"; + if (d.closing) return d.allPorts ? "正在关闭全部投口" : "正在关闭投放门"; + if (!d.opened) return d.allPorts ? "正在打开全部投口" : "正在打开投放门"; + if (d.secondsLeft <= 5) return "即将关闭投放门"; + return t("delivery"); + } + + function deliverySafetyText() { + var d = state.delivery || {}; + if (d.baselineWaiting) return "正在锁定称重基准,完成后将自动开门,请暂时不要投放物品。"; + if (d.closing) return d.allPorts ? "请确认手部和物品已离开所有投口,正在等待下位机返回称重结果。" : "请确认手部和物品已离开投口,正在等待下位机返回称重结果。"; + if (d.delayClose) return d.allPorts ? "已延时关门,倒计时结束后将自动关闭全部投口。" : "已延时关门,倒计时结束后将自动关闭投放门。"; + if (d.secondsLeft <= 5) return "投放门即将关闭,请勿将手伸入投口。"; + return "请在倒计时结束前完成投递,也可以手动关闭投放门。"; + } + + function renderSuccess() { + var s = state.session || {}; + var r = s.result || {}; + return ( + '
' + + topbar("本次投递") + + '
' + + '
' + + '

' + + escapeHtml(t("success")) + + "

" + + '

10 秒后自动返回首页

' + + renderSuccessPortCards(r) + + '
' + + '" + + "
" + + "
" + + "
" + + "
" + ); + } + + function successPortDetails(result) { + var details = result && Array.isArray(result.portDetails) && result.portDetails.length + ? result.portDetails + : [{ + port: result && result.port ? result.port : 1, + garbageTypeName: deliveryGarbageName(result && result.typeKey, result && result.port), + weight: result && result.weight != null ? result.weight : 0 + }]; + if (result && Array.isArray(result.ports) && result.ports.length) { + var visiblePorts = result.ports.map(function (port) { return Number(port); }); + details = details.filter(function (item) { + return visiblePorts.indexOf(Number(item.port)) >= 0; + }); + } + return details; + } + + function renderSuccessPortCards(result) { + var details = successPortDetails(result); + return ( + '
' + + details.map(function (item) { + return ( + '
' + + '
' + + escapeHtml(item.port + "号投口") + + "
" + + '
垃圾类型' + + escapeHtml(item.garbageTypeName || "-") + + "
" + + '
重量' + + escapeHtml(kgCompact(item.weight)) + + "
" + + "
" + ); + }).join("") + + "
" + ); + } + + function renderAdminLogin() { + var pin = state.adminLogin.value || ""; + var showPassword = Boolean(state.adminLogin.showPassword); + var pinText = pin ? (showPassword ? pin : "*".repeat(pin.length)) : t("passwordPlaceholder"); + var adminMetaSeparator = isEnglish() ? " / " : ";"; + return ( + '
' + + topbar(t("admin")) + + '
' + + '" + + "
" + + "
" + ); + } + + function renderAdminKeypad() { + var keys = ["1", "2", "3", "4", "5", "6", "7", "8", "9"]; + return ( + '
' + + keys.map(function (key) { + return '"; + }).join("") + + '" + + '' + + '" + + "
" + ); + } + + function renderSettings() { + return ( + '
' + + topbar(t("settingsTitle"), '") + + '
' + + '" + + '
' + + renderSettingsTab() + + "
" + + "
" + + renderPortSettingDialog() + + "
" + ); + } + + function tabButton(tab, label) { + return '"; + } + + function renderSettingsTab() { + if (state.activeTab === "device") return renderDeviceSettings(); + if (state.activeTab === "ports") return renderPortSettings(); + if (state.activeTab === "cache") return renderCacheSettings(); + if (state.activeTab === "face") return renderFaceSettings(); + return renderSystemSettings(); + } + + function renderDeviceSettings() { + return ( + '
' + + '

' + escapeHtml(t("deviceParams")) + '

' + + field(t("deviceNo"), "config.device.deviceNo", state.config.device.deviceNo || "") + + field(t("terminalImei"), "config.device.terminalId", state.config.device.terminalId) + + serialPathField() + + "
" + + renderMaintenanceSettings() + + renderDeviceDisabledSettings() + + '

' + escapeHtml(t("deviceCheck")) + "

" + + '
' + + '" + + '" + + '" + + "
" + + renderTestResults() + + (state.lowerVersion ? '

' + escapeHtml(t("firmwareVersion")) + ":" + escapeHtml(localizedReadStatus(state.lowerVersion)) + "

" : "") + + (state.lowerImei ? '

' + escapeHtml(t("lowerImei")) + ":" + escapeHtml(localizedReadStatus(state.lowerImei)) + "

" : "") + + "
" + + "
" + ); + } + + function serialPathField() { + var current = String(state.config.device.serialPath || "/dev/ttyS4"); + var serialState = state.serialPorts || { loaded: false, loading: false, items: [], message: "" }; + var items = Array.isArray(serialState.items) ? serialState.items.slice() : []; + var hasCurrent = items.some(function (item) { return item.path === current; }); + if (!hasCurrent) { + items.unshift({ path: current, readable: false, writable: false, configuredOnly: true }); + } + if (!serialState.loaded && !serialState.loading) { + serialState.loading = true; + state.serialPorts = serialState; + window.setTimeout(function () { refreshSerialPorts(false); }, 0); + } + var options = items.map(function (item) { + var permission = item.configuredOnly + ? "当前配置,未检测到" + : item.readable && item.writable ? "可读写" : "需获取权限"; + return '"; + }).join(""); + var hint = serialState.loading + ? "正在扫描安卓设备串口…" + : serialState.message || "选择串口后,请点击“重新启动自检”验证连接。"; + return ( + '
" + + '
" + + '
' + + '

' + escapeHtml(hint) + "

" + ); + } + + function refreshSerialPorts(notify) { + var bridge = nativeBridge(); + var serialState = state.serialPorts || { loaded: false, loading: false, items: [], message: "" }; + serialState.loading = true; + state.serialPorts = serialState; + try { + if (!bridge || typeof bridge.listSerialPorts !== "function") { + throw new Error("当前版本未加载串口扫描模块"); + } + var result = JSON.parse(String(bridge.listSerialPorts("{}") || "{}")); + if (!result.ok) throw new Error(result.error || "串口扫描失败"); + var seen = {}; + serialState.items = (Array.isArray(result.ports) ? result.ports : []).filter(function (item) { + var path = String(item && item.path || ""); + if (!/^\/dev\/[A-Za-z0-9._/-]+$/.test(path) || seen[path]) return false; + seen[path] = true; + item.path = path; + return true; + }); + serialState.message = serialState.items.length + ? "已发现 " + serialState.items.length + " 个串口设备。" + : "未发现串口设备,已保留当前配置。"; + if (notify) showToast(serialState.message); + } catch (error) { + serialState.items = []; + serialState.message = "串口扫描失败:" + (error.message || error); + if (notify) showToast(serialState.message); + } + serialState.loaded = true; + serialState.loading = false; + state.serialPorts = serialState; + render(); + } + + function renderMaintenanceSettings() { + return ( + '

' + escapeHtml(t("maintenanceMode")) + "

" + + '

' + + (state.deviceStatus.maintenance ? escapeHtml(t("maintenanceOnDesc")) : escapeHtml(t("maintenanceOffDesc"))) + + "

" + + '
' + + '" + + "
" + + "
" + ); + } + + function renderDeviceDisabledSettings() { + return ( + '

' + escapeHtml(t("deviceDisabledTitle")) + "

" + + '

' + + (state.deviceStatus.deviceDisabled ? escapeHtml(t("deviceDisabledOnDesc")) : escapeHtml(t("deviceDisabledOffDesc"))) + + "

" + + '
' + + '" + + (state.deviceStatus.deviceDisabled ? '" : "") + + "
" + + "
" + ); + } + + function renderPortSettings() { + var draft = ensureSettingsDraft(); + var cfg = draft.config; + var options = allPhysicalPorts(); + return ( + '
' + + '

' + escapeHtml(t("portCount")) + "

" + + '
" + + '

' + escapeHtml(t("portTypeStatus")) + "

" + + '
' + + allPhysicalPorts().map(renderPortSettingButton).join("") + + "
" + + "
" + + "
" + ); + } + + function renderPortSettingButton(port) { + var draft = ensureSettingsDraft(); + var cfg = draft.config; + var enabled = port <= Number(cfg.portCount); + var typeKey = cfg.portTypes[port] || "other"; + return ( + '" + ); + } + + function renderPortSettingDialog() { + var draft = ensureSettingsDraft(); + var cfg = draft.config; + var port = Number(state.portSettingsPort || 0); + var enabled = port && port <= Number(cfg.portCount); + if (state.activeTab !== "ports") return ""; + if (!enabled) return ""; + return renderPortSettingEditor(port); + } + + function renderPortSettingEditor(port) { + var draft = ensureSettingsDraft(); + var cfg = draft.config; + var liveInfo = ensurePortStatus(port); + var typeKey = cfg.portTypes[port] || "other"; + return ( + '
' + + '" + + "
" + ); + } + + function openPortSettings(target) { + var draft = ensureSettingsDraft(); + var port = Number(target.getAttribute("data-port")); + if (!port || port > Number(draft.config.portCount || 0)) return; + state.portSettingsPort = port; + render(); + scheduleSettingsPortTelemetry([port], 60); + } + + function closePortSettings() { + state.portSettingsPort = 0; + render(); + } + + function renderCacheSettings() { + if (!state.config.localBackup) state.config.localBackup = { enabled: false, path: "" }; + return ( + '
' + + '

' + escapeHtml(t("localBackupMode")) + "

" + + '

' + + (state.config.localBackup.enabled ? escapeHtml(t("localBackupOnDesc")) : escapeHtml(t("localBackupOffDesc"))) + + "

" + + '
' + + '" + + "
" + + '

' + escapeHtml(t("localBackupStorageHint")) + "

" + + '

' + escapeHtml(t("backupPath")) + ":" + + escapeHtml(state.config.localBackup.path || t("generatedAfterEnable")) + + "

" + + '

' + escapeHtml(t("localRecord")) + ":" + + escapeHtml(recordCountLabel(state.localBackups.length)) + + "

" + + "
" + + '

' + escapeHtml(t("cacheRules")) + "

" + + '
' + + field(t("retainDays"), "config.cache.retainDays", state.config.cache.retainDays, "number") + + field(t("retryIntervalHours"), "config.cache.retryIntervalHours", state.config.cache.retryIntervalHours, "number") + + "
" + + '
' + + '" + + '" + + "
" + + '

' + escapeHtml(t("pendingQueue")) + "(" + state.offlineQueue.length + ")

" + + renderQueue() + + "
" + + "
" + ); + } + + function renderFaceSettings() { + var faceEnrollment = state.faceEnrollment || { employeeNo: "", name: "", selectedFaceId: "", capturing: false, message: "" }; + var people = Array.isArray(state.facePeople) ? state.facePeople : []; + var selectedPerson = people.filter(function (person) { + return String(person.faceId || "") === String(faceEnrollment.selectedFaceId || ""); + })[0] || null; + var peopleRows = people.length + ? people.map(function (person) { + return ( + '' + escapeHtml(person.name || "-") + '' + + '' + escapeHtml(person.employeeNo || "-") + '' + + '' + (person.registered ? "已录入" : "未录入") + '' + + '' + + '' + + '' + + '' + ); + }).join("") + : '暂无人员,请先新增人员信息'; + return ( + '
' + + '

人脸匹配阈值

' + + '

数值越高越严格。允许范围 0.50~0.95,当前建议值为 0.75。

' + + '
' + + '
' + + '
' + + '

第一步:新增人员信息

' + + '

填写工号和姓名并保存人员,再到人员列表中录入人脸。

' + + '
' + + '
' + + '
' + + '
' + + '
' + + (selectedPerson ? '

第二步:录入人脸

' + + '

当前人员:' + escapeHtml(selectedPerson.name) + '(' + escapeHtml(selectedPerson.employeeNo) + ')。请让本人正对设备摄像头,确保摄像头前只有一人,然后点击开始采集。

' + + (faceEnrollment.message ? '

' + escapeHtml(faceEnrollment.message) + '

' : '') + + (faceEnrollment.capturing + ? '

SeetaFace6 人脸采集

' + + '

请保持正脸、光线均匀,并确保画面中只有当前人员。

' + + '
正在调取摄像头画面…
' + + '

' + escapeHtml((state.faceRecognition && state.faceRecognition.message) || "请正对摄像头") + '

' + + '
' + + '
' + : '
' + + '
') + + '
' : '') + + '

人员管理

' + + '

当前共 ' + people.length + ' 人。未录入人员可点击“录入人脸”继续第二步。

' + + '
' + + '
' + + peopleRows + '
姓名工号人脸状态操作
' + + "
" + ); + } + + function renderSystemSettings() { + var logEnabled = isLogEnabled(); + return ( + '
' + + '

' + escapeHtml(t("languageSystem")) + '

' + + '
" + + '

' + escapeHtml(t("changePassword")) + "

" + + '
" + + renderPasswordChangePanel() + + "
" + + '

' + escapeHtml(t("systemTools")) + "

" + + '
" + + "
" + + "
" + ); + } + + function renderPasswordChangePanel() { + var flow = state.passwordChange; + if (!flow || !flow.open) return ""; + var labels = { + current: t("passwordCurrentPrompt"), + next: t("passwordNextPrompt"), + confirm: t("passwordConfirmPrompt") + }; + var value = flow.step === "current" ? flow.current : flow.step === "next" ? flow.next : flow.confirm; + return ( + '
' + + '
' + + '
' + + escapeHtml(labels[flow.step] || labels.current) + + "
" + + '
' + + escapeHtml(maskPin(value)) + + "
" + + (flow.message ? '

' + escapeHtml(flow.message) + "

" : "") + + "
" + + '
' + + [1, 2, 3, 4, 5, 6, 7, 8, 9].map(function (num) { + return '"; + }).join("") + + '" + + '' + + '" + + "
" + + '
' + + '" + + '" + + "
" + + "
" + ); + } + + function field(label, path, value, type) { + var inputType = type || "text"; + return ( + '
' + ); + } + + function draftField(label, path, value, type) { + var inputType = type || "text"; + return ( + '
' + ); + } + + function readonlyField(label, value) { + return ( + '
' + + escapeHtml(value == null ? "" : String(value)) + + "
" + ); + } + + function toggle(label, path, value, help) { + return ( + '
' + + '
' + + escapeHtml(label) + + "" + + (help ? "" + escapeHtml(help) + "" : "") + + "
" + + '' + + "
" + ); + } + + function draftToggle(label, path, value, help) { + return ( + '
' + + '
' + + escapeHtml(label) + + "" + + (help ? "" + escapeHtml(help) + "" : "") + + "
" + + '' + + "
" + ); + } + + function renderTestResults() { + if (!state.testResults) return ""; + return ( + '
' + + state.testResults.map(function (item) { + return '
' + escapeHtml(item.label) + "" + escapeHtml(item.ok ? t("pass") : t("fail")) + "
"; + }).join("") + + "
" + ); + } + + function renderQueue() { + var queue = sanitizeOfflineQueue(state.offlineQueue); + if (queue.length !== state.offlineQueue.length) state.offlineQueue = queue; + if (!queue.length) return '

' + escapeHtml(t("noPendingData")) + "

"; + return ( + '
' + + queue.map(function (item) { + return ( + '
' + + '
' + + escapeHtml(item.kindText) + + "" + + '
' + + escapeHtml(nowText(item.createdAt)) + + " / " + + escapeHtml(item.summary) + + "
" + + '' + + escapeHtml(item.status || "queued") + + "" + + "
" + ); + }).join("") + + "
" + ); + } + + function beginStartupCheck() { + window.clearTimeout(timers.boot); + state.screen = "boot"; + state.check = defaultCheck(); + state.check.running = true; + render(); + requestConnectionCheck("startup"); + } + + function finishStartupCheck(available, skipped) { + window.clearTimeout(timers.boot); + state.check.completed = true; + state.check.running = false; + state.check.available = Boolean( + available && + state.check.serial && + state.check.network + ); + state.check.skipped = skipped; + savePersisted(); + render(); + if (state.check.available) { + timers.boot = window.setTimeout(function () { + if (state.screen === "boot" && state.check.completed && state.check.available) { + enterHomeAfterStartup(false); + } + }, STARTUP_RESULT_HOLD_MS); + } + } + + function enterHomeAfterStartup(skipped) { + window.clearTimeout(timers.boot); + connectionCheckRequestId += 1; + state.check.skipped = Boolean(skipped); + state.screen = "home"; + savePersisted(); + render(); + } + + function hasValidTerminalId() { + return /^\d{10,20}$/.test(String(state.config.device.terminalId || "").trim()); + } + + function refreshNetworkState(networkState, logChange) { + var changed = false; + var networkOnline = Boolean(networkState && networkState.networkOnline); + var tcpConnected = Boolean(networkState && networkState.tcpConnected); + var httpReachable = Boolean(networkState && networkState.httpReachable); + if ( + state.deviceStatus.networkOnline !== networkOnline || + state.deviceStatus.tcpConnected !== tcpConnected || + state.deviceStatus.httpReachable !== httpReachable + ) { + state.deviceStatus.networkOnline = networkOnline; + state.deviceStatus.tcpConnected = tcpConnected; + state.deviceStatus.httpReachable = httpReachable; + changed = true; + if (logChange) { + addLog( + "状态", + networkOnline ? "网络连接恢复" : "网络连接断开", + tcpConnected ? "业务服务器可连接" : "业务服务器不可连接" + ); + } + } + return changed; + } + + function requestConnectionCheck(mode) { + var bridge = nativeBridge(); + var requestId = ++connectionCheckRequestId; + if (mode === "startup") state.check.attempt += 1; + if (!bridge || typeof bridge.requestConnectionCheck !== "function") { + handleConnectionCheck({ requestId: requestId, mode: mode, error: "原生检测接口不可用" }); + return; + } + bridge.requestConnectionCheck(JSON.stringify({ + requestId: requestId, + mode: mode, + path: state.config.device.serialPath, + baudRate: Number(state.config.device.baudRate || 9600), + host: state.config.server.tcpHost, + port: Number(state.config.server.tcpPort || DEFAULT_REPORT_PORT), + pingHost: "www.baidu.com", + timeoutSeconds: 2 + })); + window.clearTimeout(timers.boot); + timers.boot = window.setTimeout(function () { + if (requestId !== connectionCheckRequestId) return; + handleConnectionCheck({ requestId: requestId, mode: mode, error: "检测超时" }); + }, 4500); + } + + function handleConnectionCheck(payload) { + var mode = payload && payload.mode || "startup"; + if (!payload || Number(payload.requestId || 0) !== connectionCheckRequestId) return; + window.clearTimeout(timers.boot); + if (mode === "network") { + applyNetworkTestResult(payload); + return; + } + state.deviceStatus.serialOnline = Boolean(payload.serialOnline); + refreshNetworkState(payload, false); + state.check.serial = state.deviceStatus.serialOnline; + state.check.network = Boolean( + state.deviceStatus.networkOnline && + state.deviceStatus.tcpConnected && + state.deviceStatus.httpReachable + ); + render(); + if (state.check.serial && state.check.network) { + if (!hasValidTerminalId()) addLog("串口", "串口在线但尚未获取IMEI"); + finishStartupCheck(true, false); + } else if (state.check.attempt < STARTUP_CHECK_MAX_ATTEMPTS) { + timers.boot = window.setTimeout(function () { requestConnectionCheck("startup"); }, 500); + } else { + finishStartupCheck(false, false); + } + } + + function requestPortTelemetry(ports) { + var sent = false; + (Array.isArray(ports) && ports.length ? ports : allPhysicalPorts()).forEach(function (port) { + var payload = serialPayload({ port: port }); + sent = nativeBooleanCallSilent("queryWeight", payload) || sent; + sent = nativeBooleanCallSilent("queryFill", payload) || sent; + sent = nativeBooleanCallSilent("queryTemperature", payload) || sent; + }); + return sent; + } + + function enterSelectScreen() { + state.screen = "select"; + render(); + startSelectTimeout(); + } + + function cancelSettingsPortTelemetry() { + window.clearTimeout(timers.settingsTelemetry); + } + + function scheduleSettingsPortTelemetry(ports, delay) { + cancelSettingsPortTelemetry(); + timers.settingsTelemetry = window.setTimeout(function () { + refreshSettingsPortTelemetry(ports); + }, delay == null ? 120 : delay); + } + + function refreshSettingsPortTelemetry(ports) { + var targetPorts; + var index = 0; + var sent = false; + targetPorts = (Array.isArray(ports) && ports.length ? ports : enabledPorts()) + .map(function (port) { return Number(port); }) + .filter(function (port) { return port >= 1 && port <= MAX_PORT_COUNT; }); + if (!targetPorts.length) return; + cancelSettingsPortTelemetry(); + function queryNextPort() { + var port; + if (state.screen !== "settings" || state.activeTab !== "ports") return; + if (index >= targetPorts.length) { + if (sent) addLog("串口", "设置页读取投口实时数据", targetPorts.length + "个投口"); + return; + } + port = targetPorts[index]; + index += 1; + sent = requestPortTelemetry([port]) || sent; + timers.settingsTelemetry = window.setTimeout(queryNextPort, 90); + } + queryNextPort(); + } + + function failAuthAndReturn(message) { + state.auth = { status: "failed", message: message || "会员鉴权失败" }; + addLog("窗口", "身份鉴权失败", state.auth.message); + render(); + window.clearTimeout(timers.authFail); + timers.authFail = window.setTimeout(function () { + if (state.screen === "auth" && state.auth.status === "failed") { + goHome(); + } + }, 1800); + } + + function authPriority(source) { + if (source === "face") return 3; + if (source === "card") return 2; + return 1; + } + + function authFailureMessage(source, fallback) { + if (source === "employee") return "工号错误"; + if (source === "card") return "卡片识别错误"; + if (source === "face") return "人脸识别失败"; + return fallback || "身份鉴权失败"; + } + + function openEmployeeLogin() { + if (!state.deviceStatus.serialOnline) { + showToast("串口异常,无法选择工号"); + return; + } + state.employeeLogin = { open: true, employeeNo: "", message: "" }; + render(); + } + + function closeEmployeeLogin() { + state.employeeLogin = { open: false, employeeNo: "", message: "" }; + render(); + } + + function selectEmployeeForLogin(target) { + if (!state.deviceStatus.serialOnline || !state.employeeLogin || !state.employeeLogin.open) return; + var employeeNo = String(target.getAttribute("data-employee-no") || "").trim(); + if (!employeeNo || !employeeLoginPeople().some(function (person) { return person.employeeNo === employeeNo; })) return; + state.employeeLogin.employeeNo = employeeNo; + state.employeeLogin.message = ""; + render(); + } + + function submitEmployeeLogin() { + var reason; + if (!state.deviceStatus.serialOnline) { + state.employeeLogin.message = "串口异常,无法使用工号登录"; + render(); + return; + } + var value = String((state.employeeLogin && state.employeeLogin.employeeNo) || "").trim(); + if (!value || !employeeLoginPeople().some(function (person) { return person.employeeNo === value; })) { + state.employeeLogin.message = "请选择人员"; + render(); + return; + } + reason = homeBlockReason(); + if (reason) { + // 鉴权前设备状态可能发生变化;保持弹窗状态一致并直接展示阻断原因。 + state.employeeLogin.message = reason; + render(); + return; + } + state.employeeLogin.open = false; + startAuth("employee", value, 3); + } + + function verifyFaceIdentity(identity) { + var bridge = nativeBridge(); + if (bridge && typeof bridge.verifyFace === "function") { + try { + return JSON.parse(String(bridge.verifyFace(JSON.stringify({ faceId: identity })) || "{}")); + } catch (error) { + addLog("传输", "本地人脸校验失败", error.message || error); + } + } + return verifyNativeMember("face", identity, 4); + } + + function syncFaceData(notify) { + var bridge = nativeBridge(); + if (state.faceSync.syncing) return; + state.faceSync.syncing = true; + state.faceSync.message = "正在同步人脸数据"; + render(); + window.setTimeout(function () { + var result = { ok: false, count: 0, error: "人脸数据接口未接入" }; + try { + if (bridge && typeof bridge.syncFaceData === "function") { + result = JSON.parse(String(bridge.syncFaceData("{}") || "{}")); + } + } catch (error) { + result = { ok: false, count: 0, error: error.message || "同步失败" }; + } + state.faceSync.syncing = false; + state.faceSync.lastAt = result.ok ? Date.now() : state.faceSync.lastAt; + state.faceSync.count = Number(result.count || 0); + state.faceSync.message = result.ok ? "同步完成,共 " + state.faceSync.count + " 人" : (result.error || "同步失败"); + savePersisted(); + addLog("传输", result.ok ? "人脸数据同步完成" : "人脸数据同步失败", state.faceSync.message); + if (notify) showToast(state.faceSync.message); + render(); + }, 100); + } + + function enrollCurrentFace() { + var bridge = nativeBridge(); + if (!bridge || typeof bridge.enrollCurrentFace !== "function") { + showToast("当前版本未加载人脸识别模块"); + return; + } + var selectedFaceId = String(state.faceEnrollment.selectedFaceId || ""); + var person = (state.facePeople || []).filter(function (item) { + return String(item.faceId || "") === selectedFaceId; + })[0]; + if (!person) { + state.faceEnrollment.message = "请先新增人员,再从人员列表选择录入人脸"; + render(); + return; + } + var employeeNo = String(person.employeeNo || "").trim(); + var name = String(person.name || "").trim(); + try { + var result = JSON.parse(String(bridge.enrollCurrentFace(JSON.stringify({ + faceId: selectedFaceId, + employeeNo: employeeNo, + name: name + })) || "{}")); + state.faceEnrollment.message = result.ok ? "采集成功:" + name + "(" + employeeNo + ")" : (result.error || "采集失败"); + if (result.ok) { + state.faceRecognition.enrolledCount = Number(result.count || state.faceRecognition.enrolledCount || 0); + state.faceEnrollment.capturing = false; + refreshFacePeople(false); + } + addLog("人脸", result.ok ? "现场人脸采集成功" : "现场人脸采集失败", state.faceEnrollment.message); + showToast(state.faceEnrollment.message); + render(); + } catch (error) { + state.faceEnrollment.message = "采集失败:" + (error.message || error); + showToast(state.faceEnrollment.message); + render(); + } + } + + function refreshFacePeople(renderAfter) { + var bridge = nativeBridge(); + var result = { ok: false, people: [] }; + var registered = []; + try { + if (bridge && typeof bridge.listFacePeople === "function") { + result = JSON.parse(String(bridge.listFacePeople("{}") || "{}")); + } + } catch (error) { + result = { ok: false, people: [], error: error.message || "读取人员列表失败" }; + } + if (result.ok) { + registered = Array.isArray(result.people) ? result.people : []; + state.faceRecognition.enrolledCount = Number(result.count || registered.length || 0); + } else if (result.error) { + showToast(result.error); + } + var directory = Array.isArray(state.faceDirectory) ? state.faceDirectory.slice() : []; + registered.forEach(function (person) { + if (!directory.some(function (saved) { return String(saved.faceId) === String(person.faceId); })) { + directory.push({ faceId: person.faceId, employeeNo: person.employeeNo, name: person.name }); + } + }); + state.faceDirectory = directory; + state.facePeople = directory.map(function (person) { + var match = registered.filter(function (item) { return String(item.faceId) === String(person.faceId); })[0]; + return { + faceId: String(person.faceId || ""), + employeeNo: String(person.employeeNo || ""), + name: String(person.name || ""), + registered: Boolean(match) + }; + }); + savePersisted(); + if (renderAfter !== false) render(); + } + + function addFacePerson() { + var employeeNo = String(state.faceEnrollment.employeeNo || "").trim(); + var name = String(state.faceEnrollment.name || "").trim(); + if (!employeeNo || !name) { + showToast("请填写员工工号和姓名"); + return; + } + if ((state.faceDirectory || []).some(function (person) { return String(person.employeeNo) === employeeNo; })) { + showToast("该员工工号已存在,请在人员列表中操作"); + return; + } + var faceId = "FACE-" + employeeNo; + state.faceDirectory.push({ faceId: faceId, employeeNo: employeeNo, name: name }); + state.faceEnrollment = { employeeNo: "", name: "", selectedFaceId: faceId, capturing: false, message: "人员信息已保存,请继续采集人脸" }; + savePersisted(); + refreshFacePeople(false); + showToast("人员信息已新增,请继续录入人脸"); + render(); + } + + function resetFaceForm() { + state.faceEnrollment.employeeNo = ""; + state.faceEnrollment.name = ""; + render(); + } + + function editFacePerson(target) { + state.faceEnrollment = { + employeeNo: "", + name: "", + selectedFaceId: String(target.getAttribute("data-face-id") || ""), + capturing: false, + message: "请让该人员正对设备摄像头,然后点击“开始采集人脸”" + }; + render(); + } + + function cancelFaceEnrollment() { + state.faceEnrollment.selectedFaceId = ""; + state.faceEnrollment.capturing = false; + state.faceEnrollment.message = ""; + render(); + } + + function openFaceCapture() { + if (!state.faceEnrollment.selectedFaceId) { + showToast("请先选择需要录入人脸的人员"); + return; + } + state.faceEnrollment.capturing = true; + state.faceEnrollment.message = "正在启动 SeetaFace6,请正对摄像头"; + render(); + window.setTimeout(function () { + var bridge = nativeBridge(); + try { + if (!bridge || typeof bridge.prepareFaceEnrollment !== "function") { + throw new Error("当前版本未加载 SeetaFace6 人脸采集模块"); + } + if (bridge.prepareFaceEnrollment("{}") === false) { + throw new Error("SeetaFace6 人脸采集启动失败"); + } + syncFacePreviewLayout(true); + } catch (error) { + state.faceEnrollment.message = error.message || "SeetaFace6 人脸采集启动失败"; + addLog("人脸", "准备录入失败", state.faceEnrollment.message); + showToast(state.faceEnrollment.message); + render(); + } + }, 80); + } + + function closeFaceCapture() { + state.faceEnrollment.capturing = false; + render(); + } + + function syncFacePeoplePlaceholder() { + addLog("人脸", "同步人员信息", "服务器接口暂未开发完成"); + showToast("同步接口暂未开发完成,当前为预留功能"); + } + + function applyFaceMatchThreshold(notify) { + var requested = Number(state.config.face && state.config.face.matchThreshold); + if (!isFinite(requested)) requested = 0.75; + requested = Math.round(Math.max(0.50, Math.min(0.95, requested)) * 100) / 100; + state.config.face.matchThreshold = requested; + var bridge = nativeBridge(); + try { + if (!bridge || typeof bridge.setFaceMatchThreshold !== "function") { + throw new Error("原生人脸模块不支持动态阈值"); + } + var result = JSON.parse(String(bridge.setFaceMatchThreshold(JSON.stringify({ threshold: requested })) || "{}")); + if (!result.ok) throw new Error(result.error || "阈值设置失败"); + state.config.face.matchThreshold = Number(result.threshold || requested); + if (notify) showToast("人脸匹配阈值已设置为 " + state.config.face.matchThreshold.toFixed(2)); + return true; + } catch (error) { + if (notify) showToast(error.message || "人脸匹配阈值设置失败"); + return false; + } + } + + function saveFaceMatchThreshold() { + var input = document.querySelector("[data-face-threshold]"); + var value = Number(input && input.value); + if (!isFinite(value) || value < 0.50 || value > 0.95) { + showToast("请输入 0.50~0.95 之间的阈值"); + return; + } + state.config.face.matchThreshold = Math.round(value * 100) / 100; + if (applyFaceMatchThreshold(true)) { + savePersisted(); + render(); + } + } + + function deleteFacePerson(target) { + var bridge = nativeBridge(); + var faceId = String(target.getAttribute("data-face-id") || ""); + var registered = target.getAttribute("data-registered") === "true"; + if (!faceId) return; + if (!window.confirm("确认删除该人员及其人脸数据?")) return; + try { + if (registered && bridge && typeof bridge.deleteFacePerson === "function") { + var result = JSON.parse(String(bridge.deleteFacePerson(JSON.stringify({ faceId: faceId })) || "{}")); + if (!result.ok) throw new Error(result.error || "原生人脸数据删除失败"); + } else if (registered) { + throw new Error("当前版本未加载人脸识别模块"); + } + state.faceDirectory = (state.faceDirectory || []).filter(function (person) { return String(person.faceId) !== faceId; }); + if (String(state.faceEnrollment.selectedFaceId || "") === faceId) cancelFaceEnrollment(); + savePersisted(); + refreshFacePeople(false); + showToast("人员已删除"); + render(); + } catch (error) { + showToast("删除失败:" + (error.message || error)); + } + } + + function startAuth(source, identityRaw, identityType) { + var priority = authPriority(source); + var requestToken; + var reason = homeBlockReason(); + if (reason) { + showToast(reason); + return; + } + if (state.screen !== "home" && state.screen !== "auth") return; + if (state.screen === "auth" && priority <= Number(state.auth.priority || 0)) return; + if (!identityRaw) { + showToast(source === "face" ? "等待人脸识别结果" : source === "employee" ? "请选择工号" : "请刷 IC/ID 卡"); + return; + } + var identity = identityRaw; + if (isLikelyIcCard(identity)) { + source = "card"; + identityType = 1; + } + var resolvedIdType = serverIdentityType(source, identityType); + window.clearTimeout(timers.success); + state.screen = "auth"; + state.auth = { + status: "reading", + priority: priority, + token: Date.now() + "-" + Math.random(), + message: source === "face" ? "已识别人脸,正在核对本地人脸库。" : source === "card" ? "已读取 IC/ID 卡,正在向门口终端机鉴权。" : "请稍候,正在建立用户会话。" + }; + requestToken = state.auth.token; + render(); + window.setTimeout(function () { + if (state.screen !== "auth" || state.auth.token !== requestToken) return; + if (!state.deviceStatus.networkOnline || !state.deviceStatus.tcpConnected) { + failAuthAndReturn(t("serviceDown")); + return; + } + var member = source === "face" ? verifyFaceIdentity(identity) : verifyNativeMember(source, identity, resolvedIdType); + if (!member || !member.ok) { + failAuthAndReturn(authFailureMessage(source, member && member.error)); + return; + } + var identifiedAt = Date.now(); + state.session = { + user: { + account: member && member.memberCode ? member.memberCode : accountFromIdentity(identity), + employeeNo: member && (member.employeeNo || member.memberCode) ? (member.employeeNo || member.memberCode) : accountFromIdentity(identity), + memberId: member && member.memberId ? member.memberId : "", + name: member && member.name ? member.name : "员工", + identityRaw: identity, + identifiedAt: identifiedAt, + identifiedAtText: nowText(identifiedAt), + balance: member && member.balance != null ? Number(member.balance) : 0, + balanceText: member && member.balance != null ? String(member.balance) : "0.00" + }, + source: source, + idType: resolvedIdType + }; + openWorkInfo(true); + }, 850); + } + + function openWorkInfo(preserveSession) { + var reason = homeBlockReason(); + if (reason) { + showToast(reason); + return; + } + window.clearTimeout(timers.success); + window.clearTimeout(timers.workInfoTimeout); + window.clearTimeout(timers.selectTimeout); + if (!preserveSession) state.session = null; + state.delivery = null; + state.screen = "workInfo"; + state.workInfo = defaultWorkInfo(); + if (state.session && state.session.user) { + state.workInfo.person = state.session.user.name || ""; + } + render(); + fetchWorkContext(); + startWorkInfoTimeout(); + } + + function fetchWorkContext() { + var bridge = nativeBridge(); + var requestId = ++workContextRequestId; + startWorkInfoTimeout(); + state.workInfo.loading = true; + state.workInfo.error = ""; + state.workInfo.pickerField = ""; + render(); + window.setTimeout(function () { + var options = null; + if (requestId !== workContextRequestId || state.screen !== "workInfo") return; + if (bridge && typeof bridge.fetchWorkContext === "function") { + try { + options = normalizeWorkContext(JSON.parse(String(bridge.fetchWorkContext("{}") || "[]"))); + addLog("传输", "已获取投放信息接口数据", options.length + " 台机台"); + } catch (error) { + addLog("传输", "投放信息接口解析失败", error.message || error); + } + } + if (!options) { + options = []; + state.workInfo.error = "投放信息接口暂不可用,可手动输入机台号"; + } + if (requestId !== workContextRequestId || state.screen !== "workInfo") return; + state.workInfo.loading = false; + state.workInfo.loaded = true; + state.workInfo.options = options; + if (!state.workInfo.machineNo && options[0]) state.workInfo.machineNo = options[0].machineNo; + syncWorkInfoForMachine(false); + startWorkInfoTimeout(); + render(); + }, 350); + } + + function scheduleRender() { + window.setTimeout(function () { + render(); + }, 0); + } + + function applyWorkInfoInput(target) { + var field = target.getAttribute("data-work-field"); + var linked = false; + if (!field || !state.workInfo) return false; + startWorkInfoTimeout(); + state.workInfo[field] = target.value; + state.workInfo.error = ""; + if (field === "machineNo" && syncWorkInfoForMachine(true)) { + linked = true; + } + return linked; + } + + function openWorkPicker(target) { + var field = target.getAttribute("data-picker-field") || "machineNo"; + if (!state.workInfo || state.workInfo.loading) return; + startWorkInfoTimeout(); + state.workInfo.pickerField = field; + render(); + } + + function closeWorkPicker() { + if (!state.workInfo) return; + startWorkInfoTimeout(); + state.workInfo.pickerField = ""; + render(); + } + + function pickWorkValue(target) { + var field = target.getAttribute("data-picker-field") || ""; + var value = target.getAttribute("data-picker-value") || ""; + if (!state.workInfo || !field || !value) return; + startWorkInfoTimeout(); + state.workInfo[field] = value; + state.workInfo.error = ""; + state.workInfo.pickerField = ""; + if (field === "machineNo") syncWorkInfoForMachine(true); + render(); + } + + function submitWorkInfo() { + var info = state.workInfo || defaultWorkInfo(); + if (info.loading) { + showToast("正在获取投放信息,请稍候"); + startWorkInfoTimeout(); + return; + } + info.machineNo = cleanWorkText(info.machineNo, 48); + if (!info.machineNo) { + startWorkInfoTimeout(); + info.error = "请输入或选择机台号"; + state.workInfo = info; + render(); + return; + } + if (!state.session || !state.session.user) { + showToast("员工身份已失效,请重新登录"); + goHome(); + return; + } + window.clearTimeout(timers.success); + window.clearTimeout(timers.workInfoTimeout); + state.session.workInfo = { + machineNo: info.machineNo, + person: state.session.user.name || "", + employeeName: state.session.user.name || "", + employeeNo: state.session.user.employeeNo || state.session.user.account || "", + identifiedAt: state.session.user.identifiedAt || 0, + batch: "" + }; + addLog("窗口", "投放信息已确认", info.machineNo + " / " + state.session.workInfo.employeeName + " / " + state.session.workInfo.employeeNo); + enterSelectScreen(); + } + + function startWorkInfoTimeout() { + window.clearTimeout(timers.workInfoTimeout); + timers.workInfoTimeout = window.setTimeout(function () { + if (state.screen === "workInfo") { + showToast("填写投放信息超时"); + goHome(); + } + }, WORK_INFO_TIMEOUT_MS); + } + + function startSelectTimeout() { + window.clearTimeout(timers.selectTimeout); + timers.selectTimeout = window.setTimeout(function () { + if (state.screen === "select") { + showToast("选择投口超时"); + goHome(); + } + }, 15000); + } + + function accountFromIdentity(identity) { + var text = String(identity || "").replace(/\s+/g, ""); + var digits = text.replace(/\D+/g, ""); + if (digits.length >= 8) return digits.slice(-11) || digits; + return text.slice(0, 24) || "UNKNOWN"; + } + + function handleReaderKey(event) { + if (state.screen !== "home") return; + if (state.employeeLogin && state.employeeLogin.open) return; + if (event.ctrlKey || event.metaKey || event.altKey) return; + if (event.key === "Enter") { + event.preventDefault(); + flushReaderBuffer(); + return; + } + if (event.key === "Escape") { + readerBuffer = ""; + return; + } + if (event.key.length !== 1) return; + var now = Date.now(); + if (now - readerLastAt > 650) readerBuffer = ""; + readerLastAt = now; + readerBuffer += event.key; + window.clearTimeout(timers.reader); + timers.reader = window.setTimeout(flushReaderBuffer, 320); + } + + function flushReaderBuffer() { + var identity = readerBuffer.trim(); + readerBuffer = ""; + window.clearTimeout(timers.reader); + if (state.screen !== "home" || identity.length < 4) return; + startAuth(inferIdentitySource(identity), identity); + } + + function inferIdentitySource(identity) { + var text = String(identity || "").trim(); + if (/card|ic/i.test(text)) return "card"; + if (isLikelyIcCard(text)) return "card"; + return "qr"; + } + + function selectPort(port) { + port = Number(port); + var info; + var bridge = nativeBridge(); + if (state.screen !== "select" || state.delivery) return; + if (!state.session || !state.session.workInfo) { + window.clearTimeout(timers.selectTimeout); + addLog("窗口", "投口选择失败", "投放信息会话不存在"); + showToast("投放信息已失效,请重新登陆"); + goHome(); + return; + } + if (enabledPorts().indexOf(port) < 0) { + showToast("投口编号异常"); + startSelectTimeout(); + return; + } + info = ensurePortStatus(port); + if (!info || isPortBlocked(port)) { + var reason = portBlockReason(port); + showToast(reason ? port + "号投口" + reason : t("full")); + return; + } + state.session.selectedPort = port; + state.session.selectedType = state.config.portTypes[port] || "other"; + if (!bridge) { + showToast("原生串口模块不可用,无法打开投口"); + startSelectTimeout(); + return; + } + state.delivery = createHardwareDelivery([port], false); + window.clearTimeout(timers.selectTimeout); + state.screen = "delivery"; + render(); + startBaselineBeforeOpen(); + } + + function openAllPorts() { + var ports = enabledPorts(); + var bridge = nativeBridge(); + if (state.screen !== "select" || state.delivery) return; + if (!state.session || !state.session.workInfo) { + window.clearTimeout(timers.selectTimeout); + addLog("窗口", "全部投口打开失败", "投放信息会话不存在"); + showToast("投放信息已失效,请重新登陆"); + goHome(); + return; + } + if (ports.some(isPortBlocked)) { + showToast(t("allPortsBlocked")); + startSelectTimeout(); + return; + } + state.session.selectedPort = 0; + state.session.selectedType = "allPorts"; + if (!bridge) { + showToast("原生串口模块不可用,无法打开投口"); + startSelectTimeout(); + return; + } + state.delivery = createHardwareDelivery(ports, true); + window.clearTimeout(timers.selectTimeout); + state.screen = "delivery"; + render(); + startBaselineBeforeOpen(); + } + + function createHardwareDelivery(ports, allPorts) { + var beforeWeights = {}; + ports.forEach(function (port) { + var status = ensurePortStatus(port); + beforeWeights[port] = cleanWeightNumber(status && status.weightKnown && status.weight != null ? status.weight : 0); + }); + return { + port: allPorts ? 0 : Number(ports[0]), + ports: ports.slice(), + allPorts: Boolean(allPorts), + weightItems: {}, + pendingWeightPorts: allPorts ? ports.slice() : [], + baselinePendingPorts: ports.slice(), + beforeWeights: beforeWeights, + weightSamples: {}, + baselineWaiting: false, + secondsLeft: 30, + opened: false, + closing: false, + manualClose: false, + realHardware: true + }; + } + + function startBaselineBeforeOpen() { + var delivery = state.delivery; + var pending; + if (!delivery || !delivery.realHardware || delivery.opened || delivery.closing) return; + window.clearTimeout(timers.baselineWeight); + delivery.baselineWaiting = true; + pending = (delivery.baselinePendingPorts || []).slice(); + pending.forEach(function (port) { + if (!nativeBooleanCallSilent("queryWeight", serialPayload({ port: port }))) { + delivery.baselinePendingPorts = delivery.baselinePendingPorts.filter(function (pendingPort) { + return Number(pendingPort) !== Number(port); + }); + addLog("串口", "投放前重量查询发送失败", port + "号投口 / 使用缓存值"); + } + }); + render(); + if (!delivery.baselinePendingPorts.length) { + openHardwareDeliveryAfterBaseline(false); + return; + } + timers.baselineWeight = window.setTimeout(function () { + openHardwareDeliveryAfterBaseline(true); + }, BASELINE_WEIGHT_TIMEOUT_MS); + } + + function openHardwareDeliveryAfterBaseline(timedOut) { + var delivery = state.delivery; + var ports; + var failed = false; + if (!delivery || !delivery.realHardware || delivery.opened || delivery.closing || !delivery.baselineWaiting) return; + window.clearTimeout(timers.baselineWeight); + ports = (delivery.ports || []).slice(); + if (timedOut && delivery.baselinePendingPorts.length) { + addLog("串口", "投放前重量读取超时", delivery.baselinePendingPorts.join("、") + "号投口 / 使用缓存值"); + } + delivery.baselineWaiting = false; + delivery.baselinePendingPorts = []; + ports.forEach(function (port) { + if (!nativeBooleanCall("openThrowDoor", serialPayload({ port: port }))) { + failed = true; + addLog("串口", delivery.allPorts ? "全部投口开门失败" : "开门指令发送失败", port + "号投口"); + } + }); + if (failed) { + nativeBooleanCallSilent("closeThrowDoor", serialPayload({ closeAll: true, ports: allPhysicalPorts() })); + state.delivery = null; + state.deviceStatus.serialOnline = false; + state.screen = "select"; + showToast(delivery.allPorts ? t("openAllPortsFailed") : "串口开门指令发送失败,请检查连接后重试"); + render(); + startSelectTimeout(); + return; + } + state.deviceStatus.serialOnline = true; + delivery.opened = true; + if (delivery.allPorts) showToast(t("allPortsOpened")); + render(); + startDeliveryTimer(); + } + + function captureDeliveryBaseline(payload) { + var delivery = state.delivery; + var port = Number(payload && payload.port || 0); + var pending; + var weight; + if (!delivery || delivery.closing || !payload || !payload.queryResponse || !port) return false; + pending = delivery.baselinePendingPorts || []; + if (pending.indexOf(port) < 0) return false; + weight = payload.afterWeight != null ? Number(payload.afterWeight) : Number(payload.weight); + if (!isFinite(weight)) return false; + delivery.beforeWeights[port] = cleanWeightNumber(Math.max(0, weight)); + delivery.baselinePendingPorts = pending.filter(function (pendingPort) { + return Number(pendingPort) !== port; + }); + addLog("串口", "锁定投放前重量", port + "号投口 / " + delivery.beforeWeights[port] + "kg"); + if (delivery.baselineWaiting && !delivery.baselinePendingPorts.length) { + openHardwareDeliveryAfterBaseline(false); + } + return true; + } + + function startDeliveryTimer() { + window.clearInterval(timers.delivery); + timers.delivery = window.setInterval(function () { + if (!state.delivery || state.delivery.closing) return; + state.delivery.secondsLeft -= 1; + if (state.delivery.secondsLeft <= 0) { + closeDoor(false); + } else { + render(); + } + }, 1000); + } + + function delayCloseDoor() { + if (!state.delivery || state.delivery.closing || !state.delivery.opened || state.delivery.delayClose) return; + state.delivery.secondsLeft = DELAY_CLOSE_SECONDS; + state.delivery.delayClose = true; + addLog("窗口", "已延时关门", "倒计时重置为" + DELAY_CLOSE_SECONDS + "秒"); + showToast("已延时关门,倒计时已重置为" + DELAY_CLOSE_SECONDS + "秒"); + render(); + startDeliveryTimer(); + } + + function deliveryWeightPorts() { + if (!state.delivery) return []; + if (state.delivery.allPorts) return (state.delivery.ports || allPhysicalPorts()).slice(); + return [Number(state.delivery.port || state.session.selectedPort || 1)]; + } + + function closeDoor(manual) { + var bridge = nativeBridge(); + var closePorts = allPhysicalPorts(); + var weightPorts; + if (!state.delivery || state.delivery.closing || !state.delivery.opened) return; + window.clearInterval(timers.delivery); + state.delivery.manualClose = Boolean(manual); + weightPorts = deliveryWeightPorts(); + if (!bridge || !nativeBooleanCall("closeThrowDoor", serialPayload({ closeAll: true, ports: closePorts }))) { + state.deviceStatus.serialOnline = false; + state.delivery.closing = false; + state.delivery.secondsLeft = Math.max(5, Number(state.delivery.secondsLeft || 0)); + addLog("串口", "关门指令发送失败", "全部投口"); + showToast("串口关门指令发送失败,请重试"); + render(); + startDeliveryTimer(); + return; + } + state.delivery.closing = true; + render(); + startStableWeightSampling(weightPorts); + showToast("已发送关门指令,等待秤盘稳定"); + startWeightTimeout(weightPorts); + } + + function completeHardwareDelivery(item) { + var port; + var pendingPorts; + if (!item || !state.session || !state.delivery) return; + if (item.queryResponse) { + item = collectStableWeightSample(item); + if (!item) return; + } + if (state.delivery.allPorts) { + port = Number(item.port || 0); + if (state.delivery.ports.indexOf(port) < 0) { + addLog("串口", "忽略非本次投口称重", item.port + "号投口"); + return; + } + pendingPorts = state.delivery.pendingWeightPorts || []; + if (pendingPorts.indexOf(port) < 0 && state.delivery.weightItems && state.delivery.weightItems[port]) { + addLog("串口", "忽略重复称重回包", port + "号投口"); + return; + } + state.delivery.weightItems[port] = item; + state.delivery.pendingWeightPorts = pendingPorts.filter(function (pendingPort) { + return Number(pendingPort) !== port; + }); + if (!state.delivery.pendingWeightPorts.length) { + completeDeliveryWithWeights({ + allPorts: true, + ports: state.delivery.ports, + items: state.delivery.weightItems + }); + } + return; + } + if (item.port && Number(item.port) !== Number(state.delivery.port)) { + addLog("串口", "忽略非本次投口称重", item.port + "号投口"); + return; + } + completeDeliveryWithWeights(item); + } + + function completeDelivery() { + if (!state.delivery || !state.session) return; + completeDeliveryWithWeights({}); + } + + function completeDeliveryWithWeights(item) { + if (!state.delivery || !state.session) return; + window.clearTimeout(timers.weightTimeout); + window.clearTimeout(timers.weightSample); + var repeatAfterClose = Boolean(state.delivery.repeatAfterClose); + var isAllPorts = Boolean((item && item.allPorts) || state.delivery.allPorts); + var ports = isAllPorts ? ((item && item.ports) || state.delivery.ports || allPhysicalPorts()).slice() : []; + var port = isAllPorts ? 0 : Number(item.port || (state.delivery && state.delivery.port) || state.session.selectedPort || 1); + var typeKey = isAllPorts ? "allPorts" : state.config.portTypes[port] || state.session.selectedType || "other"; + var portStatus = isAllPorts ? null : ensurePortStatus(port); + var beforeWeights = state.delivery.beforeWeights || {}; + var weights = isAllPorts + ? resolveAllDeliveryWeights(ports, (item && item.items) || state.delivery.weightItems || {}, beforeWeights) + : resolveDeliveryWeights(portStatus, item, beforeWeights[port]); + var portDetails = isAllPorts ? weights.details || [] : [deliveryPortDetail(port, weights)]; + var work = sessionWorkInfo(); + if (!isAllPorts && portStatus) { + portStatus.weight = weights.after; + portStatus.weightKnown = true; + } + var record = { + id: "D" + Date.now(), + kind: "delivery", + kindText: "投递记录", + createdAt: Date.now(), + status: "pending", + summary: (isAllPorts ? t("allPorts") : port + " 号投口") + " / " + kg(weights.delta) + " / " + deliveryGarbageName(typeKey, port), + payload: { + deviceNo: String(state.config.device.deviceNo || ""), + deviceUniqueId: String(state.config.device.terminalId || state.lowerImei || ""), + deviceName: "星元智灵回收箱", + deliveryTime: nowText(Date.now()), + createdAt: Date.now(), + employeeName: work.employeeName || work.person || "", + employeeNo: work.employeeNo || "", + identifiedAt: work.identifiedAt || 0, + idType: state.session.idType || 0, + idContent: (state.session.user && state.session.user.identityRaw) || "", + machineNo: work.machineNo, + person: work.person, + productionBatch: work.batch, + port: port, + ports: isAllPorts ? ports : [port], + garbageTypeName: deliveryGarbageName(typeKey, port), + typeCode: deliveryGarbageCode(typeKey, port), + portDetails: portDetails, + beforeWeight: weights.before, + afterWeight: weights.after, + weight: weights.delta + } + }; + var uploaded = reportDeliveryResult(record); + record.status = uploaded ? "sent" : "queued"; + if (!uploaded) enqueue(record); + state.session.result = { + port: port, + ports: isAllPorts ? ports : [port], + allPorts: isAllPorts, + typeKey: typeKey, + weight: weights.delta, + portDetails: portDetails, + uploaded: uploaded, + backupRecord: record + }; + state.delivery = null; + state.screen = "success"; + savePersisted(); + saveResultLocalBackup(); + if (repeatAfterClose) { + showToast("投口已关闭,请继续下一次投放"); + openWorkInfo(true); + return; + } + render(); + timers.success = window.setTimeout(function () { + goHome(); + }, 10000); + } + + function startWeightTimeout(ports) { + var targetPorts = Array.isArray(ports) ? ports.slice() : [Number(ports)]; + window.clearTimeout(timers.weightTimeout); + timers.weightTimeout = window.setTimeout(function () { + if (!state.delivery || !state.delivery.closing) return; + if (state.delivery.allPorts) { + var sampledItems = stableWeightItemsFromSamples(targetPorts); + addLog("串口", "全部投口称重回包超时", "未返回投口按0kg记录"); + showToast("称重回包超时,未返回投口按0kg记录"); + completeDeliveryWithWeights({ + allPorts: true, + ports: targetPorts, + items: Object.assign({}, sampledItems, state.delivery.weightItems || {}), + timeout: true + }); + return; + } + if (Number(state.delivery.port) !== Number(targetPorts[0])) return; + var sampledItem = stableWeightItemFromSamples(targetPorts[0]); + if (sampledItem) { + addLog("串口", "称重采样不足,使用已有稳定值", targetPorts[0] + "号投口"); + completeDeliveryWithWeights(sampledItem); + return; + } + addLog("串口", "称重回包超时,未记录新增重量", targetPorts[0] + "号投口"); + showToast("称重回包超时,未记录新增重量"); + completeDeliveryWithWeights({ port: targetPorts[0], timeout: true }); + }, 10000); + } + + function startStableWeightSampling(ports) { + var round = 0; + var targetPorts = ports.slice(); + window.clearTimeout(timers.weightSample); + function sampleNextRound() { + if (!state.delivery || !state.delivery.closing) return; + targetPorts.forEach(function (port) { + if (!nativeBooleanCallSilent("queryWeight", serialPayload({ port: port }))) { + addLog("串口", "称重查询指令发送失败", port + "号投口"); + } + }); + round += 1; + if (round < WEIGHT_SAMPLE_COUNT) { + timers.weightSample = window.setTimeout(sampleNextRound, WEIGHT_SAMPLE_INTERVAL_MS); + } + } + timers.weightSample = window.setTimeout(sampleNextRound, WEIGHT_SETTLE_DELAY_MS); + } + + function collectStableWeightSample(item) { + var delivery = state.delivery; + var port = Number(item && item.port || 0); + var value = Number(item && item.afterWeight != null ? item.afterWeight : item && item.weight); + if (!delivery || !port || !isFinite(value)) return null; + if (!delivery.weightSamples) delivery.weightSamples = {}; + if (!delivery.weightSamples[port]) delivery.weightSamples[port] = []; + delivery.weightSamples[port].push(Math.max(0, value)); + if (delivery.weightSamples[port].length < WEIGHT_SAMPLE_COUNT) return null; + return stableWeightItemFromSamples(port); + } + + function stableWeightItemFromSamples(port) { + var samples = state.delivery && state.delivery.weightSamples && state.delivery.weightSamples[port]; + var sorted; + var value; + if (!samples || !samples.length) return null; + sorted = samples.slice().sort(function (a, b) { return a - b; }); + value = cleanWeightNumber(sorted[Math.floor(sorted.length / 2)]); + return { + port: Number(port), + weight: value, + beforeWeight: value, + afterWeight: value, + queryResponse: true, + stableSamples: samples.length + }; + } + + function stableWeightItemsFromSamples(ports) { + var items = {}; + ports.forEach(function (port) { + var item = stableWeightItemFromSamples(port); + if (item) items[port] = item; + }); + return items; + } + + function resolveDeliveryWeights(portStatus, item, lockedBeforeWeight) { + var hasLockedBefore = isFinite(Number(lockedBeforeWeight)); + var before = hasLockedBefore + ? Number(lockedBeforeWeight) + : Number(portStatus && portStatus.weight != null ? portStatus.weight : 0); + var after = before; + var delta = 0; + var currentBefore = before; + var itemWeight = item && isFinite(Number(item.weight)) ? Number(item.weight) : null; + var itemBefore = item && isFinite(Number(item.beforeWeight)) ? Number(item.beforeWeight) : null; + var itemAfter = item && isFinite(Number(item.afterWeight)) ? Number(item.afterWeight) : null; + var hasThrowWeightRange = item && item.range != null; + if (itemBefore != null && itemAfter != null && (hasThrowWeightRange || itemAfter > itemBefore)) { + before = itemBefore; + after = itemAfter; + delta = hasThrowWeightRange && itemWeight != null && itemWeight > 0 ? itemWeight : Math.max(0, itemAfter - itemBefore); + } else if (itemAfter != null && itemAfter > currentBefore) { + after = itemAfter; + delta = itemAfter - currentBefore; + } else if (hasThrowWeightRange && itemWeight != null && itemWeight > 0) { + delta = itemWeight; + after = before + delta; + } + before = cleanWeightNumber(before); + delta = Math.max(0, cleanWeightNumber(delta)); + after = cleanWeightNumber(Math.max(before, after)); + if (after === before && delta > 0) after = cleanWeightNumber(before + delta); + return { + before: before, + after: after, + delta: Math.max(0, cleanWeightNumber(delta || after - before)) + }; + } + + function deliveryPortDetail(port, weights) { + var typeKey = state.config.portTypes[port] || "other"; + weights = weights || { before: 0, after: 0, delta: 0 }; + return { + port: port, + garbageTypeName: garbageName(typeKey, port), + typeId: garbageCode(typeKey, port), + beforeWeight: cleanWeightNumber(weights.before), + afterWeight: cleanWeightNumber(weights.after), + weight: cleanWeightNumber(weights.delta) + }; + } + + function resolveAllDeliveryWeights(ports, items, beforeWeights) { + var total = { before: 0, after: 0, delta: 0 }; + var details = []; + (Array.isArray(ports) && ports.length ? ports : allPhysicalPorts()).forEach(function (port) { + var status = ensurePortStatus(port); + var weights = resolveDeliveryWeights( + status, + items && items[port] ? items[port] : { port: port, timeout: true }, + beforeWeights && beforeWeights[port] + ); + status.weight = weights.after; + status.weightKnown = true; + total.before += weights.before; + total.after += weights.after; + total.delta += weights.delta; + details.push(deliveryPortDetail(port, weights)); + }); + return { + before: cleanWeightNumber(total.before), + after: cleanWeightNumber(total.after), + delta: cleanWeightNumber(total.delta), + details: details + }; + } + + function reportDeliveryResult(record) { + var bridge = nativeBridge(); + if (bridge && typeof bridge.reportXingyuanDelivery === "function") { + return nativeUploadCall("reportXingyuanDelivery", record.payload); + } + if (bridge && typeof bridge.reportDeliveryResult === "function") { + return nativeUploadCall("reportDeliveryResult", record.payload); + } + addLog("传输", "投放结果上报接口不可用", record.summary); + return false; + } + + function sessionBalance() { + var user = state.session && state.session.user; + if (!user) return 0; + return numericMoney(user.balanceText != null ? user.balanceText : user.balance); + } + + function refreshRewardFromServer(balanceBefore) { + window.setTimeout(function () { + if (!state.session || !state.session.result || state.screen !== "success") return; + if (state.session.user && state.session.user.guest) { + state.session.result.rewardPending = false; + state.session.result.reward = 0; + saveResultLocalBackup(balanceBefore); + render(); + return; + } + var user = state.session.user || {}; + var member = verifyNativeMember(state.session.source, user.identityRaw, state.session.idType); + if (!member || !member.ok || member.balance == null) { + state.session.result.rewardPending = false; + state.session.result.reward = 0; + saveResultLocalBackup(balanceBefore); + addLog("传输", "奖励余额刷新失败", (member && member.error) || "未返回余额"); + render(); + return; + } + var afterBalance = numericMoney(member.balance); + var reward = Math.round((afterBalance - numericMoney(balanceBefore)) * 100) / 100; + state.session.user.balance = afterBalance; + state.session.user.balanceText = String(member.balance); + state.session.result.balanceAfter = afterBalance; + state.session.result.reward = reward; + state.session.result.rewardPending = false; + saveResultLocalBackup(afterBalance); + addLog("传输", "奖励余额刷新", "投放后余额 " + plainMoney(afterBalance) + " / 奖励 " + plainMoney(reward)); + render(); + }, 1400); + } + + function localBackupEnabled() { + return Boolean(state.config.localBackup && state.config.localBackup.enabled); + } + + function saveResultLocalBackup() { + if (!localBackupEnabled() || !state.session || !state.session.result || state.session.result.backupSaved) return; + var result = state.session.result; + var record = result.backupRecord; + if (!record || !record.payload) return; + var work = sessionWorkInfo(); + var resultDetails = Array.isArray(result.portDetails) && result.portDetails.length ? result.portDetails : []; + var payloadDetails = Array.isArray(record.payload.portDetails) && record.payload.portDetails.length ? record.payload.portDetails : []; + var details = result.allPorts && (payloadDetails.length || resultDetails.length) + ? (payloadDetails.length ? payloadDetails : resultDetails) + : [{ + port: Number(record.payload.port || result.port || 1), + garbageTypeName: record.payload.garbageTypeName, + typeId: record.payload.typeCode, + weight: record.payload.weight, + beforeWeight: record.payload.beforeWeight, + afterWeight: record.payload.afterWeight + }]; + details.forEach(function (detail, index) { + var port = Number(detail.port || 1); + var typeKey = state.config.portTypes[port] || result.typeKey || "other"; + var backup = { + id: record.id + "-P" + port + "-" + index, + time: nowText(record.createdAt), + deliveryTime: nowText(record.createdAt), + timestamp: record.createdAt, + deviceNo: String(state.config.device.deviceNo || ""), + deviceUniqueId: String(state.config.device.terminalId || state.lowerImei || ""), + deviceName: "星元智灵回收箱", + machineNo: work.machineNo || record.payload.machineNo || "", + person: work.employeeName || work.person || record.payload.employeeName || record.payload.person || "", + employeeNo: work.employeeNo || record.payload.employeeNo || "", + productionBatch: work.batch || record.payload.productionBatch || "", + port: String(port), + garbageTypeName: backupGarbageName(detail, typeKey, port), + typeId: detail.typeId || detail.typeCode || garbageCode(typeKey, port), + weight: formatBackupWeight(detail.weight), + beforeWeight: formatBackupWeight(detail.beforeWeight), + afterWeight: formatBackupWeight(detail.afterWeight), + uploadStatus: record.status || "sent" + }; + state.localBackups.unshift(backup); + persistNativeLocalBackup(backup); + }); + if (state.localBackups.length > 500) state.localBackups.length = 500; + result.backupSaved = true; + savePersisted(); + } + + function formatBackupWeight(value) { + return String(cleanWeightNumber(value || 0)); + } + + function backupGarbageName(detail, typeKey, port) { + var name = String((detail && detail.garbageTypeName) || "").trim(); + return name && !isAllPortsName(name) ? name : garbageName(typeKey, port); + } + + function isAllPortsName(name) { + return ["全部投口", "All Chutes"].indexOf(String(name || "").trim()) >= 0; + } + + function backupUserText(user) { + user = user || {}; + var account = String(user.account || "GUEST"); + var name = String(user.name || ""); + return name ? account + "/" + name : account; + } + + function persistNativeLocalBackup(backup) { + var bridge = nativeBridge(); + if (!bridge || typeof bridge.saveLocalBackup !== "function") { + addLog("存储", "本地备份已写入应用缓存", backup.id); + return false; + } + try { + var path = String(bridge.saveLocalBackup(JSON.stringify({ record: backup })) || ""); + if (path) { + if (!state.config.localBackup) state.config.localBackup = { enabled: false, path: "" }; + state.config.localBackup.path = path; + addLog("存储", "本地备份已保存", path); + return true; + } + } catch (error) { + addLog("存储", "本地备份写入异常", error.message || error); + return false; + } + addLog("存储", "本地备份写入失败", backup.id); + return false; + } + + function exportLogs() { + var bridge = nativeBridge(); + var payload = { + exportedAt: Date.now(), + exportedAtText: nowText(Date.now()), + appVersion: state.config.appVersion, + deviceNo: state.config.device.deviceNo || "", + terminalId: state.config.device.terminalId || "", + logs: state.logs.slice().reverse() + }; + var path = ""; + if (!bridge || typeof bridge.exportLogs !== "function") { + addLog("存储", "日志导出失败", "原生导出接口未接入"); + showToast(t("logExportFailed")); + return false; + } + try { + path = String(bridge.exportLogs(JSON.stringify(payload)) || ""); + if (path) { + addLog("存储", "日志已导出", path); + showToast(t("logExported") + ":" + path); + return true; + } + } catch (error) { + addLog("存储", "日志导出异常", error.message || error); + } + showToast(t("logExportFailed")); + return false; + } + + function loadNativeLocalBackups() { + var bridge = nativeBridge(); + if (!bridge || typeof bridge.readLocalBackups !== "function") return; + try { + var raw = bridge.readLocalBackups("{}"); + var records = JSON.parse(String(raw || "[]")); + if (!Array.isArray(records) || !records.length) return; + var seen = {}; + state.localBackups.forEach(function (item) { + if (item && item.id) seen[item.id] = true; + }); + records.reverse().forEach(function (item) { + if (item && item.id && !seen[item.id]) { + state.localBackups.unshift(item); + seen[item.id] = true; + } + }); + if (state.localBackups.length > 500) state.localBackups.length = 500; + addLog("存储", "已读取本地备份", records.length + " 条"); + } catch (error) { + addLog("存储", "读取本地备份失败", error.message || error); + } + } + + function canUpload() { + return ( + state.deviceStatus.networkOnline && + state.deviceStatus.tcpConnected + ); + } + + function nativeBridge() { + return window.XingyuanNative || window.ZhitouNative || null; + } + + function nativeLogKind(method) { + if (/report|Heartbeat|configure|verifyMember/.test(method)) return "传输"; + if (/Serial|Imei|Version|ThrowDoor|ReceiveDoor|Weight/.test(method)) return "串口"; + return "窗口"; + } + + function nativeLogMessage(method) { + var labels = { + configure: "配置上报连接", + startHeartbeat: "启动心跳 1001", + reportMemberVerify: "发送会员校验 4301", + reportDelivery: "发送投递记录 6011", + reportXingyuanDelivery: "发送星元智灵投放结果", + reportDeliveryResult: "发送投放结果", + fetchWorkContext: "获取投放信息", + exportLogs: "导出日志", + reportDeviceStatus: "发送设备状态 2003", + reportAlarm: "发送报警 6006", + verifyMember: "请求会员校验 4301", + checkSerial: "串口自检", + getLowerImei: "读取下位机IMEI", + getLowerVersion: "读取下位机版本", + openThrowDoor: "发送开门指令", + openReceiveDoor: "发送清运门开启指令", + closeThrowDoor: "发送关门指令", + queryWeight: "查询下位机称重", + pingHost: "Ping 网络 www.baidu.com", + openSystemSettings: "打开系统设置", + openFileManager: "打开文件管理" + }; + return labels[method] || method; + } + + function nativeCall(method, payload) { + var bridge = nativeBridge(); + if (!bridge || typeof bridge[method] !== "function") { + addLog(nativeLogKind(method), nativeLogMessage(method) + "未接入"); + return false; + } + try { + bridge[method](JSON.stringify(payload || {})); + addLog(nativeLogKind(method), nativeLogMessage(method) + "已调用"); + return true; + } catch (error) { + addLog(nativeLogKind(method), nativeLogMessage(method) + "调用失败", error.message || error); + return false; + } + } + + function nativeUploadCall(method, payload) { + var bridge = nativeBridge(); + var result; + if (!bridge || typeof bridge[method] !== "function") { + addLog(nativeLogKind(method), nativeLogMessage(method) + "未接入"); + return false; + } + try { + result = bridge[method](JSON.stringify(payload || {})); + if (result === false || String(result).toLowerCase() === "false") { + addLog(nativeLogKind(method), nativeLogMessage(method) + "失败"); + return false; + } + addLog(nativeLogKind(method), nativeLogMessage(method) + "已调用"); + return true; + } catch (error) { + addLog(nativeLogKind(method), nativeLogMessage(method) + "调用失败", error.message || error); + return false; + } + } + + function nativeBooleanCall(method, payload) { + var bridge = nativeBridge(); + if (!bridge || typeof bridge[method] !== "function") { + addLog(nativeLogKind(method), nativeLogMessage(method) + "未接入"); + return false; + } + try { + var ok = bridge[method](JSON.stringify(payload || {})) === true; + addLog(nativeLogKind(method), nativeLogMessage(method) + (ok ? "成功" : "失败")); + return ok; + } catch (error) { + addLog(nativeLogKind(method), nativeLogMessage(method) + "异常", error.message || error); + return false; + } + } + + function nativeBooleanCallSilent(method, payload) { + var bridge = nativeBridge(); + if (!bridge || typeof bridge[method] !== "function") return false; + try { + return bridge[method](JSON.stringify(payload || {})) === true; + } catch (error) { + return false; + } + } + + function nativeCallSilent(method, payload) { + var bridge = nativeBridge(); + if (!bridge || typeof bridge[method] !== "function") return false; + try { + bridge[method](JSON.stringify(payload || {})); + return true; + } catch (error) { + return false; + } + } + + function serialPayload(extra) { + var payload = { + path: state.config.device.serialPath || "/dev/ttyS4", + baudRate: Number(state.config.device.baudRate || 9600) + }; + Object.keys(extra || {}).forEach(function (key) { + payload[key] = extra[key]; + }); + return payload; + } + + function configureNativeReporter() { + nativeCall("configure", { + host: state.config.server.tcpHost, + port: Number(state.config.server.tcpPort || DEFAULT_REPORT_PORT), + terminalId: state.config.device.terminalId || "", + appVersion: state.config.appVersion, + defaultMemberId: state.config.server.defaultMemberId || DEFAULT_MEMBER_ID, + packageType: state.config.server.packageType || DEFAULT_PACKAGE_TYPE, + httpBase: state.config.server.httpBase || DEFAULT_HTTP_BASE + }); + } + + function startNativeHeartbeat() { + configureNativeReporter(); + nativeCall("startHeartbeat", {}); + } + + function serverIdentityType(source, explicitType) { + var parsed = Number(explicitType || 0); + if (parsed > 0 && parsed <= 255) return parsed; + if (source === "card") return 1; + if (source === "employee") return 3; + if (source === "face") return 4; + return 2; + } + + function isLikelyIcCard(identity) { + return /^[0-9A-F]{8}$/i.test(String(identity || "").trim()); + } + + function decodeIdentityPart(value) { + try { + return decodeURIComponent(String(value || "").replace(/\+/g, "%20")); + } catch (error) { + return String(value || ""); + } + } + + function pickQueryIdentity(text) { + var source = String(text || ""); + var question = source.indexOf("?"); + var query = question >= 0 ? source.slice(question + 1) : source; + var parts = query.split(/[&;]/); + var keys = { + yueng: true, + idcontent: true, + memberkey: true, + resident_id: true, + qrcode: true, + qr: true, + code: true + }; + var i; + for (i = 0; i < parts.length; i += 1) { + var part = parts[i]; + var eq = part.indexOf("="); + if (eq <= 0) continue; + var key = part.slice(0, eq).toLowerCase(); + if (keys[key]) return decodeIdentityPart(part.slice(eq + 1)).trim(); + } + return ""; + } + + function normalizeIdentityForServer(source, identityRaw, idType) { + var type = serverIdentityType(source, idType); + var text = String(identityRaw || "").trim(); + if (type === 1 || source === "card") { + return text.replace(/\s+/g, ""); + } + text = text.replace(/\s+/g, ""); + if (/^yueng=/i.test(text)) { + return decodeIdentityPart(text.slice(text.indexOf("=") + 1)).trim(); + } + var picked = pickQueryIdentity(text); + return picked || text; + } + + function verifyNativeMember(source, identityRaw, idType) { + if (!canUpload()) { + addLog("传输", "会员校验未发送", t("serviceDown")); + return { ok: false, error: t("serviceDown") }; + } + var bridge = nativeBridge(); + if (!bridge || typeof bridge.verifyMember !== "function") { + addLog("传输", "会员校验 4301 未接入"); + return { ok: false, error: "未接入会员鉴权接口" }; + } + try { + var resolvedType = serverIdentityType(source, idType); + var identityContent = normalizeIdentityForServer(source, identityRaw, resolvedType); + if (identityContent !== String(identityRaw || "").trim()) { + addLog("传输", "二维码内容已按协议清洗", identityContent); + } + addLog("传输", "发送会员校验 4301", identityContent || ""); + var raw = bridge.verifyMember(JSON.stringify({ + idType: resolvedType, + idContent: identityContent || "", + source: source + })); + var result = JSON.parse(String(raw || "{}")); + addLog("传输", result.ok ? "会员校验 4302 通过" : "会员校验失败", result.ok ? result.memberCode || result.memberId || "" : result.error || ""); + return result; + } catch (error) { + addLog("传输", "会员校验返回解析失败", error.message || error); + return { ok: false, error: "会员鉴权返回解析失败" }; + } + } + + function reportNativeMemberVerify(source, identityRaw, idType) { + if (!canUpload()) { + addLog("传输", "会员校验未发送", t("serviceDown")); + return false; + } + var resolvedType = serverIdentityType(source, idType); + return nativeCall("reportMemberVerify", { + idType: resolvedType, + idContent: normalizeIdentityForServer(source, identityRaw, resolvedType) || "", + source: source + }); + } + + function rangeForPort(port) { + return sensorHeight(port); + } + + function buildStatusPayload() { + return { + temperature: portTemperature(1), + humidity: 0, + signal: 30, + deviceEnable: state.deviceStatus.maintenance || state.deviceStatus.deviceDisabled ? 0 : 1, + lat: "0", + lng: "0", + altitude: "0", + ports: enabledPorts().map(function (port) { + var info = state.deviceStatus.ports[port]; + var typeKey = state.config.portTypes[port] || "other"; + return { + port: port, + garbageType: numericGarbageCode(typeKey, port), + range: rangeForPort(port), + weight: Number(info.weight || 0), + temperature: portTemperature(port), + overflow: Boolean(info.overflow) + }; + }) + }; + } + + function reportNativeDeviceStatus() { + if (!state.deviceStatus.networkOnline) { + addLog("传输", "设备状态 2003 未发送", t("serviceDown")); + return false; + } + configureNativeReporter(); + return nativeCall("reportDeviceStatus", buildStatusPayload()); + } + + function reportNativeDelivery(record, source) { + if (!canUpload()) { + addLog("传输", "投递记录 6011 未发送", t("serviceDown")); + return false; + } + addLog("传输", "准备投递记录 6011", record.payload.port + "号投口 / " + record.payload.weight + "kg"); + var resolvedType = serverIdentityType(source, record.payload.idType || (state.session && state.session.idType)); + return nativeCall("reportDelivery", { + idType: resolvedType, + idContent: normalizeIdentityForServer(source, record.payload.identityRaw || record.payload.account || "", resolvedType) || "", + port: record.payload.port, + garbageType: Number(record.payload.typeCode), + range: rangeForPort(record.payload.port), + beforeWeight: record.payload.beforeWeight, + afterWeight: record.payload.afterWeight, + throwWeight: record.payload.weight, + reward: record.payload.reward, + createdAt: record.createdAt + }); + } + + function reportNativeAlarms() { + if (!canUpload()) return false; + var alarms = []; + enabledPorts().forEach(function (port) { + var info = state.deviceStatus.ports[port]; + var typeKey = state.config.portTypes[port] || "other"; + if (info.overflow) { + alarms.push({ + port: port, + garbageType: numericGarbageCode(typeKey, port), + errorCode: 301, + isError: 1, + temperature: portTemperature(port) + }); + } + }); + if (!alarms.length) return false; + return nativeCall("reportAlarm", { alarms: alarms }); + } + + function enqueue(item) { + item = sanitizeQueueItem(item); + if (!item) { + addLog("传输", "缓存入队失败", "数据格式异常"); + return; + } + state.offlineQueue.unshift(item); + if (state.offlineQueue.length > 500) state.offlineQueue.length = 500; + savePersisted(); + } + + function retryQueuedItem(item) { + if (!item || item.kind !== "delivery") { + addLog("传输", "跳过未知缓存类型", item && item.kind ? item.kind : "unknown"); + return false; + } + return reportDeliveryResult(item); + } + + function retryCache() { + var before; + var sent = 0; + var remaining = []; + cleanupCache(false); + state.offlineQueue = sanitizeOfflineQueue(state.offlineQueue); + before = state.offlineQueue.length; + if (!before) { + savePersisted(); + showToast(t("cacheRetryEmpty")); + render(); + return; + } + if (!canUpload()) { + showToast(t("networkUnavailableQueue")); + render(); + return; + } + state.offlineQueue.forEach(function (item) { + item.status = "retrying"; + if (retryQueuedItem(item)) { + sent += 1; + } else { + item.status = "queued"; + remaining.push(item); + } + }); + state.offlineQueue = remaining; + savePersisted(); + if (sent) { + showToast( + t("cacheRetryPrefix") + + sent + + t("cacheRetrySuffix") + + (remaining.length ? t("cacheRetryRemainPrefix") + remaining.length + t("cacheRetryRemainSuffix") : "") + ); + } else { + showToast(t("cacheRetryFail")); + } + render(); + } + + function repeatDelivery() { + if (!state.delivery || state.delivery.closing || !state.delivery.opened) return; + state.delivery.repeatAfterClose = true; + addLog("窗口", "再次投放", "先安全关闭当前投口并记录本次重量"); + closeDoor(true); + } + + function cleanupCache(notify) { + var retainMs = Number(state.config.cache.retainDays || 30) * 24 * 60 * 60 * 1000; + var cutoff = Date.now() - retainMs; + var before = state.offlineQueue.length; + state.offlineQueue = state.offlineQueue.filter(function (item) { + return item.createdAt >= cutoff; + }); + savePersisted(); + if (notify) showToast(t("cacheCleanupPrefix") + (before - state.offlineQueue.length) + t("cacheCleanupSuffix")); + } + + function goHome() { + window.clearInterval(timers.delivery); + window.clearTimeout(timers.success); + window.clearTimeout(timers.authFail); + window.clearTimeout(timers.workInfoTimeout); + window.clearTimeout(timers.selectTimeout); + window.clearTimeout(timers.baselineWeight); + window.clearTimeout(timers.weightTimeout); + window.clearTimeout(timers.weightSample); + cancelSettingsPortTelemetry(); + window.clearInterval(timers.admin); + state.screen = "home"; + state.settingsDraft = null; + state.portSettingsPort = 0; + state.delivery = null; + state.auth = { status: "idle", message: "" }; + state.employeeLogin = { open: false, employeeNo: "", message: "" }; + render(); + } + + function openAdminLogin() { + window.clearInterval(timers.admin); + state.screen = "adminLogin"; + state.adminLogin = { tries: 0, secondsLeft: 30, message: "", value: "", showPassword: false }; + render(); + timers.admin = window.setInterval(function () { + state.adminLogin.secondsLeft -= 1; + if (state.adminLogin.secondsLeft <= 0) { + showToast(t("adminTimeout")); + goHome(); + } else if (state.screen === "adminLogin") { + render(); + } + }, 1000); + } + + function submitAdmin() { + var password = state.adminLogin.value || ""; + if (isAdminPasswordValid(password)) { + window.clearInterval(timers.admin); + state.screen = "settings"; + state.activeTab = "device"; + state.settingsDraft = createSettingsDraft(); + render(); + return; + } + state.adminLogin.tries += 1; + state.adminLogin.message = t("passwordWrong"); + state.adminLogin.value = ""; + state.adminLogin.showPassword = false; + if (state.adminLogin.tries >= 5) { + showToast(t("adminTooManyErrors")); + goHome(); + return; + } + render(); + } + + function appendAdminDigit(digit) { + if (state.screen !== "adminLogin") return; + if ((state.adminLogin.value || "").length >= 12) return; + state.adminLogin.value = (state.adminLogin.value || "") + String(digit); + state.adminLogin.message = ""; + render(); + } + + function backspaceAdminDigit() { + if (state.screen !== "adminLogin") return; + state.adminLogin.value = (state.adminLogin.value || "").slice(0, -1); + state.adminLogin.message = ""; + render(); + } + + function clearAdminDigits() { + if (state.screen !== "adminLogin") return; + state.adminLogin.value = ""; + state.adminLogin.message = ""; + render(); + } + + function toggleAdminPasswordVisibility() { + if (state.screen !== "adminLogin") return; + state.adminLogin.showPassword = !state.adminLogin.showPassword; + render(); + } + + function runNetworkTest() { + state.testResults = [{ label: t("networkState"), ok: false, pending: true }]; + showToast("正在后台检测网络..."); + render(); + requestConnectionCheck("network"); + } + + function applyNetworkTestResult(networkState) { + var pingOk = Boolean(networkState && networkState.pingOk); + var tcpOk = Boolean(networkState && networkState.tcpConnected); + refreshNetworkState(networkState || {}, false); + state.testResults = [ + { label: "Ping www.baidu.com", ok: pingOk }, + { label: t("tcpReportParams") + " " + state.config.server.tcpHost + ":" + state.config.server.tcpPort, ok: tcpOk }, + { label: t("networkState"), ok: state.deviceStatus.networkOnline } + ]; + addLog("窗口", state.deviceStatus.networkOnline ? "网络测试通过" : "网络测试失败", tcpOk ? "业务 TCP 可连接" : "业务 TCP 不可连接"); + savePersisted(); + showToast(state.deviceStatus.networkOnline ? t("networkTestOk") : t("networkTestFail")); + render(); + } + + function checkLowerVersion() { + window.clearTimeout(timers.lowerVersion); + var bridge = nativeBridge(); + try { + if (bridge && typeof bridge.requestLowerVersion === "function") { + state.lowerVersion = "读取中..."; + showToast(t("readingLowerVersion")); + bridge.requestLowerVersion(JSON.stringify(serialPayload())); + timers.lowerVersion = window.setTimeout(function () { + if (state.lowerVersion === "读取中...") { + state.lowerVersion = "获取失败"; + showToast(t("lowerVersionMissing")); + render(); + } + }, 3500); + render(); + return; + } + } catch (error) { + addLog("串口", "读取下位机版本异常", error.message || error); + } + state.lowerVersion = "获取失败"; + showToast(t("lowerVersionRequestFail")); + render(); + } + + function applyLowerImei(imei, silent) { + imei = String(imei || "").trim(); + if (!/^\d{10,20}$/.test(imei)) return false; + window.clearTimeout(timers.lowerImei); + state.deviceStatus.serialOnline = true; + var changed = state.config.device.terminalId !== imei || state.lowerImei !== imei; + state.config.device.terminalId = imei; + state.lowerImei = imei; + if (changed) { + savePersisted(); + configureNativeReporter(); + window.setTimeout(function () { + reportNativeDeviceStatus(); + }, 120); + } + addLog("串口", "获取下位机IMEI", imei); + if (!silent) showToast(t("lowerImeiReceived") + ":" + imei); + render(); + return true; + } + + function applyLowerVersion(version, silent) { + version = String(version || "").trim(); + if (!version) return false; + window.clearTimeout(timers.lowerVersion); + state.deviceStatus.serialOnline = true; + state.lowerVersion = version; + addLog("串口", "获取下位机版本", version); + if (!silent) showToast(t("lowerVersionReceived") + ":" + version); + render(); + return true; + } + + function requestNativeLowerImei(silent) { + var bridge = nativeBridge(); + var imei = ""; + try { + if (bridge && typeof bridge.getLowerImei === "function") { + imei = String(bridge.getLowerImei(JSON.stringify(serialPayload())) || ""); + } + } catch (error) { + addLog("串口", "读取下位机IMEI异常", error.message || error); + return false; + } + if (/^\d{10,20}$/.test(imei)) { + return applyLowerImei(imei, silent); + } + addLog("串口", "未读取到下位机IMEI", imei || "空回包"); + return false; + } + + function getLowerImei() { + window.clearTimeout(timers.lowerImei); + var bridge = nativeBridge(); + try { + if (bridge && typeof bridge.requestLowerImei === "function") { + state.lowerImei = "读取中..."; + showToast(t("readingLowerImei")); + bridge.requestLowerImei(JSON.stringify(serialPayload())); + timers.lowerImei = window.setTimeout(function () { + if (state.lowerImei === "读取中...") { + state.lowerImei = "获取失败"; + showToast(t("lowerImeiMissing")); + render(); + } + }, 3500); + render(); + return; + } + } catch (error) { + addLog("串口", "读取下位机IMEI异常", error.message || error); + } + state.lowerImei = "获取失败"; + showToast(t("lowerImeiRequestFail")); + render(); + } + + function getLowerInfo() { + window.clearTimeout(timers.lowerImei); + window.clearTimeout(timers.lowerVersion); + window.clearTimeout(timers.lowerInfo); + var bridge = nativeBridge(); + state.lowerVersion = "读取中..."; + state.lowerImei = "读取中..."; + try { + if (bridge && typeof bridge.requestLowerVersion === "function") { + bridge.requestLowerVersion(JSON.stringify(serialPayload())); + } else { + state.lowerVersion = "获取失败"; + } + if (bridge && typeof bridge.requestLowerImei === "function") { + bridge.requestLowerImei(JSON.stringify(serialPayload())); + } else { + state.lowerImei = "获取失败"; + } + } catch (error) { + addLog("串口", "读取下位机信息异常", error.message || error); + state.lowerVersion = "获取失败"; + state.lowerImei = "获取失败"; + } + showToast(t("readingLowerInfo")); + timers.lowerInfo = window.setTimeout(function () { + var changed = false; + if (state.lowerVersion === "读取中...") { + state.lowerVersion = "获取失败"; + changed = true; + } + if (state.lowerImei === "读取中...") { + state.lowerImei = "获取失败"; + changed = true; + } + if (changed) { + showToast(t("lowerInfoTimeout")); + render(); + } + }, 4000); + render(); + } + + function openPasswordChange() { + state.passwordChange = { open: true, step: "current", current: "", next: "", confirm: "", message: "" }; + render(); + } + + function passwordValueKey() { + var flow = state.passwordChange || {}; + return flow.step === "next" ? "next" : flow.step === "confirm" ? "confirm" : "current"; + } + + function appendPasswordDigit(digit) { + var flow = state.passwordChange; + if (!flow || !flow.open) return; + var key = passwordValueKey(); + if ((flow[key] || "").length >= 12) return; + flow[key] = (flow[key] || "") + String(digit); + flow.message = ""; + render(); + } + + function backspacePasswordDigit() { + var flow = state.passwordChange; + if (!flow || !flow.open) return; + var key = passwordValueKey(); + flow[key] = (flow[key] || "").slice(0, -1); + flow.message = ""; + render(); + } + + function clearPasswordDigits() { + var flow = state.passwordChange; + if (!flow || !flow.open) return; + flow[passwordValueKey()] = ""; + flow.message = ""; + render(); + } + + function nextPasswordStep() { + var flow = state.passwordChange; + if (!flow || !flow.open) return; + if (flow.step === "current") { + if (!isAdminPasswordValid(flow.current)) { + flow.current = ""; + flow.message = t("passwordCurrentWrong"); + } else { + flow.step = "next"; + flow.message = ""; + } + render(); + return; + } + if (flow.step === "next") { + if (!/^\d{4,12}$/.test(flow.next || "")) { + flow.message = t("passwordRule"); + } else { + flow.step = "confirm"; + flow.message = ""; + } + render(); + return; + } + if (String(flow.confirm) !== String(flow.next)) { + flow.confirm = ""; + flow.message = t("passwordMismatch"); + render(); + return; + } + state.config.adminPassword = String(flow.next); + state.passwordChange = { open: false, step: "current", current: "", next: "", confirm: "", message: "" }; + savePersisted(); + showToast(t("passwordChanged")); + render(); + } + + function cancelPasswordChange() { + state.passwordChange = { open: false, step: "current", current: "", next: "", confirm: "", message: "" }; + render(); + } + + function toggleLocalBackupMode() { + if (!state.config.localBackup) state.config.localBackup = { enabled: false, path: "" }; + state.config.localBackup.enabled = !state.config.localBackup.enabled; + if (state.config.localBackup.enabled) { + var bridge = nativeBridge(); + try { + if (bridge && typeof bridge.getLocalBackupPath === "function") { + state.config.localBackup.path = String(bridge.getLocalBackupPath("{}") || state.config.localBackup.path || ""); + } + } catch (error) { + addLog("存储", "读取本地备份路径失败", error.message || error); + } + } + savePersisted(); + showToast(state.config.localBackup.enabled ? t("localBackupEnabledToast") : t("localBackupDisabledToast")); + render(); + } + + function toggleMaintenanceMode() { + var enabling = !state.deviceStatus.maintenance; + state.deviceStatus.maintenance = enabling; + state.check.available = Boolean( + !enabling && + !state.deviceStatus.deviceDisabled && + state.deviceStatus.serialOnline && + state.deviceStatus.networkOnline && + state.deviceStatus.tcpConnected + ); + if (enabling) { + enabledPorts().forEach(function (port) { + nativeBooleanCallSilent("openThrowDoor", serialPayload({ port: port })); + }); + } else { + nativeBooleanCallSilent("closeThrowDoor", serialPayload()); + } + if (enabling && state.screen !== "settings") goHome(); + savePersisted(); + addLog("窗口", enabling ? "维护模式已启用" : "维护模式已关闭", enabling ? "已发送投放门开启指令" : "已发送投放门关闭指令"); + showToast(enabling ? t("maintenanceEnabledToast") : t("maintenanceDisabledToast")); + render(); + } + + function toggleDeviceDisabled() { + var enabling = !state.deviceStatus.deviceDisabled; + state.deviceStatus.deviceDisabled = enabling; + if (!enabling) { + state.deviceStatus.collectionDoorClosed = true; + } + state.check.available = Boolean( + !enabling && + !state.deviceStatus.maintenance && + state.deviceStatus.serialOnline && + state.deviceStatus.networkOnline && + state.deviceStatus.tcpConnected + ); + if (enabling && state.screen !== "settings") goHome(); + savePersisted(); + addLog("窗口", enabling ? "设备禁用已启用" : "设备禁用已关闭", enabling ? "仅允许打开清运门" : "恢复正常流程"); + showToast(enabling ? t("deviceDisabledEnabledToast") : t("deviceDisabledDisabledToast")); + render(); + } + + function openCollectionDoor() { + if (!state.deviceStatus.deviceDisabled) { + showToast(t("enableDeviceDisabledFirst")); + return; + } + if (!nativeBooleanCall("openReceiveDoor", serialPayload({ port: 1 }))) { + showToast(t("collectionDoorCommandFail")); + return; + } + state.deviceStatus.collectionDoorClosed = false; + state.check.available = false; + savePersisted(); + addLog("串口", "已发送清运门开启指令", "0x04"); + showToast(t("collectionDoorCommandSent")); + render(); + } + + function toggleDebugLog() { + if (!state.config.debugLog) state.config.debugLog = { enabled: false }; + state.config.debugLog.enabled = !isLogEnabled(); + savePersisted(); + addLog("窗口", state.config.debugLog.enabled ? "日志窗口已启用" : "日志窗口已禁用"); + showToast(state.config.debugLog.enabled ? t("debugLogEnabledToast") : t("debugLogDisabledToast")); + render(); + } + + function applyPortSettings(notify, shouldRender) { + var draft = ensureSettingsDraft(); + var livePorts = state.deviceStatus.ports || {}; + state.config.portCount = clampPortCount(draft.config.portCount || 1); + state.config.portTypes = cloneData(draft.config.portTypes); + state.config.customPortTypes = cloneData(draft.config.customPortTypes); + state.deviceStatus.ports = cloneData(draft.deviceStatus.ports); + [1, 2, 3, 4].forEach(function (port) { + if (!state.deviceStatus.ports[port]) state.deviceStatus.ports[port] = defaultPortStatus(port); + if (livePorts[port]) { + state.deviceStatus.ports[port].weight = livePorts[port].weight; + state.deviceStatus.ports[port].range = livePorts[port].range; + state.deviceStatus.ports[port].temperature = normalizeTemperatureValue(livePorts[port].temperature, defaultPortStatus(port).temperature); + } + }); + migratePortConfig(); + migratePortStatus(); + savePersisted(); + if (notify) showToast(t("portSettingsSaved")); + if (shouldRender) render(); + } + + function savePortSettings() { + applyPortSettings(true, true); + } + + function handleBoundInput(target) { + var path = target.getAttribute("data-bind"); + if (!path) return; + var root = path.indexOf("config.") === 0 ? state : state; + var value; + if (target.type === "checkbox") { + value = target.checked; + } else if (target.getAttribute("data-type") === "number") { + value = Number(target.value); + } else { + value = target.value; + } + setPath(root, path, value); + if (path === "config.portCount") state.config.portCount = clampPortCount(value || 1); + if (/^config\.customPortTypes\.\d+\.code$/.test(path)) { + var parsedCode = Math.max(0, Math.min(255, Number(value || 0))); + setPath(root, path, String(parsedCode)); + } + savePersisted(); + } + + function handleDraftInput(target) { + var path = target.getAttribute("data-draft"); + if (!path) return; + var draft = ensureSettingsDraft(); + var value; + if (target.type === "checkbox") { + value = target.checked; + } else if (target.getAttribute("data-type") === "number") { + value = Number(target.value); + } else { + value = target.value; + } + setPath(draft, path, value); + if (path === "config.portCount") { + draft.config.portCount = clampPortCount(value || 1); + if (state.portSettingsPort > draft.config.portCount) state.portSettingsPort = 0; + } + if (/^config\.customPortTypes\.\d+\.code$/.test(path)) { + var parsedCode = Math.max(0, Math.min(255, Number(value || 0))); + setPath(draft, path, String(parsedCode)); + } + applyPortSettings(false, false); + } + + function bindEvents() { + window.addEventListener("resize", function () { + syncFacePreviewLayout(isFacePreviewActive()); + }); + document.addEventListener("scroll", function () { + if (isFacePreviewActive()) { + syncFacePreviewLayout(true); + } + }, true); + + document.addEventListener("click", function (event) { + var target = event.target.closest("[data-action]"); + if (!target) return; + var action = target.getAttribute("data-action"); + if (action === "restart-check") beginStartupCheck(); + if (action === "enter-home-after-check") enterHomeAfterStartup(true); + if (action === "start-auth") startAuth(target.getAttribute("data-source") || "qr"); + if (action === "open-work-info") openWorkInfo(false); + if (action === "open-employee-login") openEmployeeLogin(); + if (action === "close-employee-login") closeEmployeeLogin(); + if (action === "select-employee") selectEmployeeForLogin(target); + if (action === "submit-employee-login") submitEmployeeLogin(); + if (action === "refresh-work-info") fetchWorkContext(); + if (action === "submit-work-info") submitWorkInfo(); + if (action === "open-work-picker") openWorkPicker(target); + if (action === "close-work-picker") closeWorkPicker(); + if (action === "pick-work-value") pickWorkValue(target); + if (action === "go-home") goHome(); + if (action === "admin-login") openAdminLogin(); + if (action === "submit-admin") submitAdmin(); + if (action === "admin-key") appendAdminDigit(target.getAttribute("data-key")); + if (action === "admin-backspace") backspaceAdminDigit(); + if (action === "admin-clear") clearAdminDigits(); + if (action === "toggle-admin-password") toggleAdminPasswordVisibility(); + if (action === "settings-tab") { + state.activeTab = target.getAttribute("data-tab") || "device"; + if (state.activeTab !== "ports") state.portSettingsPort = 0; + if (state.activeTab === "face") refreshFacePeople(false); + render(); + } + if (action === "open-port-settings") openPortSettings(target); + if (action === "close-port-settings") closePortSettings(); + if (action === "select-port") selectPort(target.getAttribute("data-port")); + if (action === "open-all-ports") openAllPorts(); + if (action === "delay-close") delayCloseDoor(); + if (action === "close-door") closeDoor(true); + if (action === "repeat-delivery") repeatDelivery(); + if (action === "toggle-local-backup") toggleLocalBackupMode(); + if (action === "toggle-maintenance") toggleMaintenanceMode(); + if (action === "toggle-device-disabled") toggleDeviceDisabled(); + if (action === "open-collection-door") openCollectionDoor(); + if (action === "toggle-debug-log") toggleDebugLog(); + if (action === "export-logs") exportLogs(); + if (action === "run-network-test") runNetworkTest(); + if (action === "check-lower-version") checkLowerVersion(); + if (action === "get-lower-imei") getLowerImei(); + if (action === "get-lower-info") getLowerInfo(); + if (action === "refresh-serial-ports") refreshSerialPorts(true); + if (action === "retry-cache") retryCache(); + if (action === "cleanup-cache") { + cleanupCache(true); + render(); + } + if (action === "enroll-current-face") enrollCurrentFace(); + if (action === "open-face-capture") openFaceCapture(); + if (action === "close-face-capture") closeFaceCapture(); + if (action === "add-face-person") addFacePerson(); + if (action === "cancel-face-enrollment") cancelFaceEnrollment(); + if (action === "sync-face-people") syncFacePeoplePlaceholder(); + if (action === "save-face-threshold") saveFaceMatchThreshold(); + if (action === "refresh-face-people") refreshFacePeople(true); + if (action === "reset-face-form") resetFaceForm(); + if (action === "edit-face-person") editFacePerson(target); + if (action === "delete-face-person") deleteFacePerson(target); + if (action === "open-password-change") openPasswordChange(); + if (action === "password-key") appendPasswordDigit(target.getAttribute("data-key")); + if (action === "password-backspace") backspacePasswordDigit(); + if (action === "password-clear") clearPasswordDigits(); + if (action === "password-next") nextPasswordStep(); + if (action === "password-cancel") cancelPasswordChange(); + if (action === "open-system-settings") nativeCall("openSystemSettings", {}); + if (action === "open-file-manager") nativeCall("openFileManager", {}); + }); + + document.addEventListener("keydown", handleReaderKey); + + document.addEventListener("change", function (event) { + var target = event.target; + if (target.matches("[data-bind]")) { + handleBoundInput(target); + if (target.type === "checkbox" || target.tagName === "SELECT") render(); + } + if (target.matches("[data-draft]")) { + handleDraftInput(target); + if (target.type === "checkbox" || target.tagName === "SELECT") render(); + } + if (target.matches("[data-work-field]")) { + if (applyWorkInfoInput(target)) scheduleRender(); + } + if (target.matches("[data-face-enroll]")) { + state.faceEnrollment[target.getAttribute("data-face-enroll")] = target.value; + } + }); + + document.addEventListener("input", function (event) { + var target = event.target; + if (target.matches("[data-bind]") && target.type !== "checkbox" && target.tagName !== "SELECT") { + handleBoundInput(target); + } + if (target.matches("[data-draft]") && target.type !== "checkbox" && target.tagName !== "SELECT") { + handleDraftInput(target); + } + if (target.matches("[data-work-field]")) { + applyWorkInfoInput(target); + } + if (target.matches("[data-face-enroll]")) { + state.faceEnrollment[target.getAttribute("data-face-enroll")] = target.value; + } + }); + + document.addEventListener("keydown", function (event) { + if (state.screen === "adminLogin" && /^[0-9]$/.test(event.key)) appendAdminDigit(event.key); + if (state.screen === "adminLogin" && event.key === "Backspace") backspaceAdminDigit(); + if (state.screen === "adminLogin" && event.key === "Enter") submitAdmin(); + if (state.screen === "home" && state.employeeLogin && state.employeeLogin.open && event.key === "Enter") submitEmployeeLogin(); + if (state.screen === "workInfo" && event.key === "Enter") submitWorkInfo(); + if (state.screen === "delivery" && event.key === "Escape") closeDoor(true); + }); + } + + function startCacheTicker() { + window.clearInterval(timers.queue); + timers.queue = window.setInterval(function () { + if (!state.offlineQueue.length) return; + var retryMs = Number(state.config.cache.retryIntervalHours || 1) * 60 * 60 * 1000; + var oldest = state.offlineQueue[state.offlineQueue.length - 1]; + if (Date.now() - oldest.createdAt >= retryMs) retryCache(); + }, 30000); + } + + function startStatusReporter() { + window.clearInterval(timers.status); + window.setTimeout(function () { + reportNativeDeviceStatus(); + reportNativeAlarms(); + }, 3000); + window.setTimeout(function () { + reportNativeDeviceStatus(); + reportNativeAlarms(); + }, 15000); + timers.status = window.setInterval(function () { + reportNativeDeviceStatus(); + reportNativeAlarms(); + }, STATUS_REPORT_INTERVAL_MS); + } + + window.ZhitouSerial = { + onNativeLog: function (kind, message) { + addLog(kind || "传输", message || ""); + }, + onConnectionCheck: function (payload) { + handleConnectionCheck(payload || {}); + }, + onImei: function (imei) { + applyLowerImei(imei, state.lowerImei !== "读取中..."); + }, + onVersion: function (version) { + applyLowerVersion(version, state.lowerVersion !== "读取中..."); + }, + onIdentity: function (type, identity) { + identity = String(identity || "").trim(); + if (!identity || state.screen !== "home") return; + addLog("串口", "收到身份输入", identity); + state.deviceStatus.serialOnline = true; + state.check.completed = true; + state.check.available = Boolean( + state.deviceStatus.networkOnline && + state.deviceStatus.tcpConnected && + !state.deviceStatus.maintenance && + !state.deviceStatus.deviceDisabled + ); + savePersisted(); + startAuth(Number(type) === 1 || isLikelyIcCard(identity) ? "card" : Number(type) === 3 ? "employee" : "qr", identity, Number(type) === 1 || isLikelyIcCard(identity) ? 1 : Number(type) || null); + }, + onFaceRecognized: function (payload) { + var face = typeof payload === "string" ? { faceId: payload } : (payload || {}); + if (!face.faceId || (state.screen !== "home" && state.screen !== "auth")) return; + addLog("窗口", "收到人脸识别结果", face.faceId); + startAuth("face", String(face.faceId), 4); + }, + onFaceStatus: function (payload) { + var face = payload || {}; + state.faceRecognition.status = String(face.status || "unknown"); + state.faceRecognition.message = String(face.message || "人脸识别状态未知"); + state.faceRecognition.enrolledCount = Number(face.enrolledCount || 0); + addLog("人脸", state.faceRecognition.message); + if (state.screen === "home" || (state.screen === "settings" && state.activeTab === "face")) render(); + }, + onThrowWeight: function (payload) { + var isDeliveryWeight = Boolean(state.delivery && state.delivery.closing); + state.deviceStatus.serialOnline = true; + addLog("串口", "收到称重回包", (payload && payload.port ? payload.port + "号投口 / " : "") + (payload && payload.weight != null ? payload.weight + "kg" : "")); + captureDeliveryBaseline(payload); + if (payload && payload.port && state.deviceStatus.ports[payload.port]) { + var info = state.deviceStatus.ports[payload.port]; + if (!isDeliveryWeight) { + if (payload.afterWeight != null) info.weight = Number(payload.afterWeight); + else if (payload.weight != null) info.weight = Number(payload.weight); + if (isFinite(Number(info.weight))) info.weightKnown = true; + } + if (payload.range != null) applyRangeReading(payload.port, payload.range); + syncSettingsDraftPortTelemetry(payload.port); + } + if (isDeliveryWeight) { + completeHardwareDelivery(payload || {}); + } else if (state.screen === "select" || state.screen === "settings") { + render(); + } + }, + onDeviceStatus: function (payload) { + if (!payload || !payload.port || !state.deviceStatus.ports[payload.port]) return; + state.deviceStatus.serialOnline = true; + var info = state.deviceStatus.ports[payload.port]; + if (payload.temperature != null) { + var nextTemperature = normalizeTemperatureValue(payload.temperature, null); + if (nextTemperature != null) { + info.temperature = nextTemperature; + } else { + addLog("串口", "忽略无效温度", payload.port + "号投口 / " + payload.temperature + "°C"); + } + } + var rangeAccepted = payload.range != null ? applyRangeReading(payload.port, payload.range) : false; + syncSettingsDraftPortTelemetry(payload.port); + addLog( + "串口", + "收到设备状态", + payload.port + "号投口 / 高度 " + (payload.range != null ? rangeAccepted ? payload.range + "cm" : "无效" : "-") + " / 温度 " + (payload.temperature != null ? payload.temperature + "°C" : "-") + ); + if (state.screen === "select" || state.screen === "settings") { + render(); + } + } + }; + window.XingyuanSerial = window.ZhitouSerial; + + function init() { + loadPersisted(); + loadNativeLocalBackups(); + addLog("窗口", "应用启动", state.config.appVersion + " / " + state.config.server.tcpHost + ":" + state.config.server.tcpPort); + bindEvents(); + applyFaceMatchThreshold(false); + startCacheTicker(); + startNativeHeartbeat(); + refreshFacePeople(false); + startStatusReporter(); + beginStartupCheck(); + } + + init(); +})(); diff --git a/app/src/main/assets/www/assets/recycle-poster-1.png b/app/src/main/assets/www/assets/recycle-poster-1.png new file mode 100644 index 0000000..07f1ef9 Binary files /dev/null and b/app/src/main/assets/www/assets/recycle-poster-1.png differ diff --git a/app/src/main/assets/www/assets/recycle-poster-2.png b/app/src/main/assets/www/assets/recycle-poster-2.png new file mode 100644 index 0000000..d3a976a Binary files /dev/null and b/app/src/main/assets/www/assets/recycle-poster-2.png differ diff --git a/app/src/main/assets/www/assets/recycle-poster-3.png b/app/src/main/assets/www/assets/recycle-poster-3.png new file mode 100644 index 0000000..652a947 Binary files /dev/null and b/app/src/main/assets/www/assets/recycle-poster-3.png differ diff --git a/app/src/main/assets/www/index.html b/app/src/main/assets/www/index.html new file mode 100644 index 0000000..c478abe --- /dev/null +++ b/app/src/main/assets/www/index.html @@ -0,0 +1,16 @@ + + + + + + + 星元智灵 + + + + +
+
+ + + diff --git a/app/src/main/assets/www/styles.css b/app/src/main/assets/www/styles.css new file mode 100644 index 0000000..dd14741 --- /dev/null +++ b/app/src/main/assets/www/styles.css @@ -0,0 +1,1923 @@ +:root { + --ink: #1d2729; + --muted: #637174; + --line: #d8e0de; + --paper: #f7faf8; + --panel: #ffffff; + --deep: #1b3134; + --teal: #23777f; + --green: #5d9f5e; + --amber: #e7a23a; + --red: #c9503f; + --blue: #447ca7; + --shadow: 0 14px 38px rgba(24, 40, 43, 0.12); +} + +* { + box-sizing: border-box; +} + +html, +body { + min-height: 100%; +} + +body { + margin: 0; + background: #e9eeec; + color: var(--ink); + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", + "Microsoft YaHei", Arial, sans-serif; +} + +button, +input, +select { + font: inherit; +} + +button { + cursor: pointer; +} + +button:disabled, +input:disabled, +select:disabled { + cursor: not-allowed; + opacity: 0.55; +} + +.app-shell { + width: min(100vw, 800px); + min-height: 100vh; + min-height: 100dvh; + margin: 0 auto; + background: var(--paper); + box-shadow: 0 0 0 1px rgba(27, 49, 52, 0.08); + overflow-x: hidden; +} + +.screen { + min-height: 100vh; + min-height: 100dvh; + display: flex; + flex-direction: column; +} + +.topbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 14px; + min-height: 74px; + padding: 14px 22px; + background: var(--deep); + color: #fff; +} + +.brand { + display: flex; + align-items: center; + gap: 12px; + min-width: 0; +} + +.brand-mark { + width: 42px; + height: 42px; + display: grid; + place-items: center; + border-radius: 8px; + background: #fff; + color: var(--deep); + font-weight: 800; +} + +.brand-title { + margin: 0; + font-size: 22px; + line-height: 1.05; + letter-spacing: 0; +} + +.brand-subtitle { + margin: 5px 0 0; + color: rgba(255, 255, 255, 0.72); + font-size: 12px; +} + +.topbar-actions { + display: flex; + flex-wrap: wrap; + justify-content: flex-end; + gap: 8px; +} + +.pill { + display: inline-flex; + align-items: center; + gap: 7px; + min-height: 30px; + padding: 5px 9px; + border-radius: 8px; + background: rgba(255, 255, 255, 0.11); + color: #fff; + font-size: 12px; + white-space: nowrap; +} + +.pill::before { + content: ""; + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--green); +} + +.pill.warn::before { + background: var(--amber); +} + +.pill.bad::before { + background: var(--red); +} + +.content { + flex: 1; + padding: 22px; +} + +.boot-screen { + background: + linear-gradient(rgba(27, 49, 52, 0.86), rgba(27, 49, 52, 0.86)), + url("./assets/recycle-poster-1.png") center / cover; + color: #fff; +} + +.boot-panel { + width: min(100%, 560px); + margin: auto; + padding: 28px; + background: rgba(255, 255, 255, 0.96); + color: var(--ink); + border-radius: 8px; + box-shadow: var(--shadow); +} + +.boot-title { + margin: 0 0 8px; + font-size: 30px; + letter-spacing: 0; +} + +.boot-desc { + margin: 0 0 20px; + color: var(--muted); + line-height: 1.7; +} + +.check-list { + display: grid; + gap: 12px; + margin: 22px 0; +} + +.check-row { + display: grid; + grid-template-columns: 1fr auto; + gap: 12px; + align-items: center; + padding: 13px 14px; + border: 1px solid var(--line); + background: #fbfdfc; + border-radius: 8px; +} + +.check-name { + font-weight: 700; +} + +.check-meta { + color: var(--muted); + font-size: 13px; +} + +.check-state { + min-width: 76px; + text-align: center; + padding: 7px 10px; + border-radius: 8px; + color: #fff; + background: var(--amber); + font-size: 13px; + font-weight: 700; +} + +.check-state.ok { + background: var(--green); +} + +.check-state.bad { + background: var(--red); +} + +.actions { + display: flex; + flex-wrap: wrap; + gap: 10px; + align-items: center; +} + +.btn { + min-height: 44px; + border: 1px solid var(--line); + border-radius: 8px; + padding: 0 16px; + background: #fff; + color: var(--ink); + font-weight: 700; +} + +.btn.primary { + border-color: var(--teal); + background: var(--teal); + color: #fff; +} + +.btn.success { + border-color: var(--green); + background: var(--green); + color: #fff; +} + +.btn.danger { + border-color: var(--red); + background: var(--red); + color: #fff; +} + +.btn.dark { + border-color: var(--deep); + background: var(--deep); + color: #fff; +} + +.btn.ghost { + background: transparent; +} + +.icon-btn { + width: 44px; + min-width: 44px; + height: 44px; + border: 1px solid var(--line); + border-radius: 8px; + background: #fff; + color: var(--ink); + font-weight: 800; +} + +.home-main { + flex: 1; + display: block; + padding: 22px; +} + +.app-log-window { + position: fixed; + top: 84px; + right: max(12px, calc((100vw - 800px) / 2 + 12px)); + z-index: 80; + width: min(430px, calc(100vw - 24px)); + max-height: 220px; + overflow: hidden; + padding: 10px; + border: 1px solid rgba(255, 255, 255, 0.18); + border-radius: 8px; + background: rgba(13, 22, 23, 0.9); + color: #fff; + box-shadow: 0 14px 30px rgba(0, 0, 0, 0.22); + pointer-events: none; +} + +.log-title { + display: flex; + justify-content: space-between; + gap: 10px; + margin-bottom: 8px; + color: rgba(255, 255, 255, 0.78); + font-size: 12px; + font-weight: 800; +} + +.log-list { + display: grid; + gap: 5px; +} + +.log-line { + display: grid; + grid-template-columns: 56px 42px minmax(0, 1fr); + gap: 6px; + align-items: start; + font-size: 11px; + line-height: 1.35; +} + +.log-time { + color: rgba(255, 255, 255, 0.55); +} + +.log-kind { + color: #9edfd6; + font-weight: 800; +} + +.log-message { + min-width: 0; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.log-empty { + color: rgba(255, 255, 255, 0.68); + font-size: 12px; +} + +.home-lower { + position: relative; + min-height: calc(100vh - 118px); + display: grid; + grid-template-rows: 1fr auto; + align-items: center; + padding: 34px; + background: var(--panel); + border: 1px solid var(--line); + border-radius: 8px; +} + +.home-prompt { + text-align: center; +} + +.prompt-title { + margin: 0; + font-size: 42px; + font-size: clamp(30px, 6vw, 52px); + line-height: 1.18; + letter-spacing: 0; +} + +.prompt-subtitle { + margin: 18px auto 0; + max-width: 520px; + color: var(--muted); + font-size: 18px; + line-height: 1.7; +} + +.home-status { + margin-top: 26px; + display: inline-flex; + align-items: center; + gap: 10px; + max-width: min(86vw, 760px); + min-height: 42px; + padding: 10px 14px; + border-radius: 8px; + background: #eef6f2; + color: #31543a; + font-weight: 800; + line-height: 1.45; +} + +.home-status.bad { + background: #f8e6e2; + color: #833225; +} + +.face-live-feedback { + width: min(100%, 560px); + margin: 12px auto 0; + padding: 10px 14px; + border-radius: 8px; + background: #fff3d8; + color: #79520c; + text-align: center; + font-size: 16px; + font-weight: 800; +} + +.face-live-feedback.bad { + background: #f8e6e2; + color: #833225; +} + +.face-recognition-indicator { + display: flex; + align-items: center; + justify-content: center; + gap: 12px; + margin: 22px auto 0; + color: var(--teal); + font-size: 20px; +} + +.camera-preview-host { + position: relative; + display: grid; + place-items: center; + overflow: hidden; + border: 4px solid #d9e5e2; + border-radius: 12px; + background: #11191b; + color: rgba(255, 255, 255, 0.7); + font-size: 14px; + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08); +} + +.camera-preview-host::after { + content: "USB CAMERA"; + position: absolute; + right: 10px; + bottom: 8px; + z-index: 2; + padding: 4px 7px; + border-radius: 5px; + background: rgba(0, 0, 0, 0.48); + color: rgba(255, 255, 255, 0.9); + font-size: 10px; + font-weight: 800; + letter-spacing: 0.08em; + pointer-events: none; +} + +.home-camera-preview { + width: 100%; + max-width: 560px; + height: 420px; + margin: 20px auto 0; +} + +.settings-camera-preview { + width: 100%; + max-width: 480px; + height: 360px; + margin: 0 auto; +} + +.face-preview-status { + margin: 12px 0 0; + text-align: center; + font-weight: 700; +} + +.face-enroll-button { + min-width: 180px; + min-height: 50px; +} + +.face-enrollment-message { + color: var(--teal); + font-weight: 800; +} + +.face-people-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; + margin-bottom: 14px; +} + +.face-people-table { + width: 100%; + border-collapse: collapse; + background: #fff; +} + +.face-people-table th, +.face-people-table td { + padding: 13px 12px; + border-bottom: 1px solid var(--line); + text-align: left; + vertical-align: middle; +} + +.face-people-table th { + color: var(--muted); + font-size: 13px; + font-weight: 800; +} + +.face-registered-badge { + display: inline-flex; + padding: 5px 9px; + border-radius: 999px; + background: #dff2e8; + color: #256344; + font-size: 12px; + font-weight: 900; +} + +.face-registered-badge.pending { + background: #fff0d4; + color: #8a5a11; +} + +.face-person-actions { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.face-person-actions .btn { + min-height: 38px; + padding: 7px 12px; +} + +.face-capture-backdrop { + position: fixed; + inset: 0; + z-index: 95; + display: flex; + align-items: center; + justify-content: center; + padding: 18px; + background: rgba(12, 26, 28, 0.76); +} + +.face-capture-dialog { + width: min(94vw, 640px); + max-height: 94vh; + overflow-y: auto; + padding: 18px; + border-radius: 12px; + background: #fff; + box-shadow: 0 24px 70px rgba(0, 0, 0, 0.32); +} + +.face-capture-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 14px; +} + +.face-capture-preview { + width: 100%; + height: 360px; + margin-top: 12px; +} + +.face-capture-inline { + margin-top: 14px; + padding: 14px; + border: 1px solid var(--line); + border-radius: 10px; + background: #f6f9f8; +} + +.face-capture-inline h3 { + margin: 0; + font-size: 20px; +} + +.face-capture-status { + margin: 12px 0 0; + color: var(--teal); + text-align: center; + font-weight: 800; +} + +.face-capture-actions { + justify-content: center; +} + +.face-empty-row { + padding: 28px 12px !important; + color: var(--muted); + text-align: center !important; +} + +.face-frame { + display: grid; + place-items: center; + width: 54px; + height: 54px; + border: 3px solid var(--teal); + border-radius: 14px; + font-size: 30px; + animation: facePulse 1.6s ease-in-out infinite; +} + +@keyframes facePulse { + 50% { box-shadow: 0 0 0 8px rgba(35, 130, 122, 0.12); } +} + +.home-actions { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; +} + +.home-login-btn { + min-width: 180px; + min-height: 56px; + font-size: 20px; +} + +.admin-entry { + position: absolute; + right: 18px; + bottom: 18px; +} + +.flow-screen.white { + background: #fff; + color: #101517; +} + +.flow-body { + flex: 1; + padding: 28px 26px; +} + +.section-title { + margin: 0 0 8px; + font-size: 26px; + letter-spacing: 0; +} + +.section-desc { + margin: 0 0 20px; + color: var(--muted); + line-height: 1.65; +} + +.user-band { + display: grid; + grid-template-columns: 1fr auto; + gap: 14px; + align-items: center; + margin-bottom: 20px; + padding: 18px 20px; + border: 1px solid var(--line); + border-radius: 8px; + background: #fbfdfc; +} + +.work-info-body { + display: flex; + flex-direction: column; +} + +.work-form { + width: min(100%, 620px); + display: grid; + gap: 16px; + margin-top: 10px; +} + +.work-field { + margin-bottom: 0; +} + +.work-field input { + min-height: 58px; + font-size: 20px; +} + +.picker-input-row { + display: grid; + grid-template-columns: minmax(0, 1fr) 112px; + gap: 10px; + align-items: stretch; +} + +.picker-input-row input { + min-width: 0; +} + +.picker-button { + min-height: 58px; + font-size: 18px; +} + +.work-picker-backdrop { + position: fixed; + /* Android 7 的旧版 WebView 不支持 inset,必须保留四边定位作为兼容写法。 */ + top: 0; + right: 0; + bottom: 0; + left: 0; + inset: 0; + z-index: 50; + display: grid; + place-items: center; + padding: 24px; + background: rgba(10, 28, 30, 0.38); +} + +.employee-login-backdrop { + width: 100vw; + height: 100vh; + height: 100dvh; + display: flex; + align-items: center; + justify-content: center; + z-index: 100; +} + +.work-picker-dialog { + width: min(92vw, 620px); + max-height: min(72vh, 560px); + overflow: auto; + padding: 18px; + border: 1px solid var(--line); + border-radius: 8px; + background: #fff; + box-shadow: var(--shadow); +} + +.employee-login-dialog { + width: min(92vw, 520px); + max-height: calc(100dvh - 48px); + margin: auto; +} + +.employee-choice-list { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; + margin: 12px 0 18px; +} + +.employee-choice { + min-height: 76px; + padding: 12px 14px; + border: 2px solid var(--line); + border-radius: 8px; + background: #f8fbfa; + color: var(--ink); + display: grid; + gap: 5px; + text-align: left; +} + +.employee-choice span { + font-size: 20px; + font-weight: 900; +} + +.employee-choice strong { + color: var(--muted); + font-size: 15px; +} + +.employee-choice.selected { + border-color: var(--teal); + background: #e5f4ef; +} + +.employee-choice:disabled { + background: #f1f3f2; + color: #98a2a3; +} + +.work-picker-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 14px; +} + +.work-picker-head h3 { + margin: 0; + font-size: 24px; +} + +.work-picker-table { + width: 100%; + border-collapse: collapse; + table-layout: fixed; +} + +.work-picker-table td { + padding: 6px; +} + +.work-picker-cell { + width: 100%; + min-height: 56px; + border: 1px solid var(--line); + border-radius: 8px; + background: #f8fbfa; + color: var(--ink); + font-size: 19px; + font-weight: 900; +} + +.work-picker-cell.selected, +.work-picker-cell:active { + border-color: var(--teal); + background: #e5f4ef; + color: #174f51; +} + +.empty-work-picker-list { + padding: 22px; + color: var(--muted); + text-align: center; +} + +.work-actions { + margin-top: 10px; +} + +.work-actions .btn { + min-height: 58px; + padding: 0 24px; + font-size: 18px; +} + +.loading-panel { + width: min(100%, 620px); +} + +.work-summary { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.work-summary-item { + min-width: 0; +} + +.work-summary-item span { + display: block; + margin-bottom: 6px; + color: var(--muted); + font-size: 14px; + font-weight: 800; +} + +.work-summary-item strong { + display: block; + overflow-wrap: anywhere; + font-size: 24px; + line-height: 1.25; +} + +.user-band strong { + font-size: 30px; + line-height: 1.25; +} + +.work-summary .work-summary-item strong { + font-size: 24px; +} + +.user-band .small { + margin-top: 6px; + font-size: 22px; +} + +.balance { + font-size: 30px; + font-weight: 800; + color: var(--teal); +} + +.ports-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; +} + +.ports-grid.port-count-1 { + grid-template-columns: minmax(0, 1fr); +} + +.ports-grid.port-count-3 { + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 10px; +} + +.ports-grid.port-count-4 { + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; +} + +.port-card { + min-height: 150px; + padding: 18px; + border: 2px solid var(--line); + border-radius: 8px; + background: #fff; + text-align: left; +} + +.ports-grid.compact .port-card { + min-height: 136px; + padding: 16px; +} + +.port-card.available { + border-color: rgba(35, 119, 127, 0.42); +} + +.port-card.available:hover { + border-color: var(--teal); + box-shadow: 0 8px 18px rgba(35, 119, 127, 0.12); +} + +.port-card.blocked { + background: #f4f5f3; + color: #7b8587; +} + +.port-head { + display: flex; + justify-content: space-between; + gap: 10px; + align-items: center; +} + +.port-number { + font-size: 28px; + font-weight: 900; +} + +.ports-grid.compact .port-number { + font-size: 24px; +} + +.port-head .small { + margin-top: 6px; + font-size: 16px; +} + +.ports-grid.compact .port-head .small { + font-size: 14px; +} + +.type-chip { + padding: 9px 13px; + border-radius: 8px; + background: #e9f4f1; + color: #285c55; + font-size: 22px; + font-weight: 900; +} + +.ports-grid.compact .type-chip { + padding: 8px 11px; + font-size: 19px; +} + +.select-actions { + margin-top: 20px; +} + +.delivery-stage { + min-height: 610px; + display: grid; + place-items: center; + text-align: center; +} + +.countdown { + margin: 18px 0 8px; + font-size: 120px; + font-size: clamp(82px, 20vw, 170px); + font-weight: 900; + line-height: 0.95; + letter-spacing: 0; +} + +.phase { + font-size: 44px; + font-size: clamp(28px, 6vw, 54px); + font-weight: 900; + letter-spacing: 0; +} + +.safety { + min-height: 58px; + margin: 18px auto 28px; + max-width: 560px; + color: #5d3528; + font-size: 20px; + line-height: 1.55; +} + +.delivery-actions { + justify-content: center; +} + +.delivery-actions .btn { + min-width: 150px; + min-height: 54px; + font-size: 18px; +} + +.success-port-result-grid { + width: min(100%, 760px); + margin: 0 auto; + display: grid; + gap: 14px; +} + +.success-port-result-grid.port-count-1 { + max-width: 360px; + grid-template-columns: 1fr; +} + +.success-port-result-grid.port-count-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.success-port-result-grid.port-count-3, +.success-port-result-grid.port-count-4 { + width: min(100%, 920px); + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.success-port-result-card { + min-height: 168px; + padding: 18px; + border: 1px solid var(--line); + border-radius: 8px; + background: #fff; + display: grid; + gap: 12px; + align-content: center; + text-align: left; + box-shadow: 0 10px 22px rgba(21, 55, 58, 0.08); +} + +.success-port-result-title { + color: var(--teal-dark); + font-size: 22px; + font-weight: 900; +} + +.success-port-result-metric { + display: grid; + grid-template-columns: 86px minmax(0, 1fr); + gap: 12px; + align-items: center; + min-height: 42px; + padding: 8px 10px; + border: 1px solid var(--line); + border-radius: 8px; + background: #f6faf8; +} + +.success-port-result-metric span { + color: var(--muted); + font-size: 15px; + font-weight: 800; +} + +.success-port-result-metric strong { + color: var(--ink); + font-size: 24px; +} + +.login-box { + width: min(100%, 460px); + margin: 54px auto 0; + padding: 22px; + background: #fff; + border: 1px solid var(--line); + border-radius: 8px; + box-shadow: var(--shadow); +} + +.field { + display: grid; + gap: 7px; + margin-bottom: 14px; +} + +.field label { + color: #334346; + font-weight: 800; + font-size: 13px; +} + +.field input, +.field select { + width: 100%; + min-height: 44px; + padding: 0 11px; + border: 1px solid var(--line); + border-radius: 8px; + background: #fff; + color: var(--ink); +} + +.serial-path-field { + grid-column: 1 / -1; +} + +.serial-path-row { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 10px; + align-items: stretch; +} + +.serial-path-row .btn { + min-width: 104px; +} + +.serial-path-hint { + margin: 0; +} + +.readonly-value { + width: 100%; + min-height: 44px; + display: flex; + align-items: center; + padding: 0 11px; + border: 1px solid var(--line); + border-radius: 8px; + background: #f6f9f7; + color: #526467; + font-weight: 800; +} + +.pin-panel { + display: grid; + gap: 8px; + margin: 16px 0 14px; +} + +.pin-label { + color: #334346; + font-weight: 800; + font-size: 13px; +} + +.pin-input-row { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 10px; + align-items: stretch; +} + +.pin-display { + width: 100%; + min-height: 58px; + display: flex; + align-items: center; + justify-content: center; + padding: 0 12px; + border: 1px solid var(--line); + border-radius: 8px; + background: #f7faf8; + color: var(--ink); + font-size: 24px; + font-weight: 900; + letter-spacing: 0; +} + +.pin-toggle-button { + min-width: 96px; + min-height: 58px; + padding: 0 14px; + white-space: nowrap; +} + +.numeric-keypad { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin: 14px 0 16px; +} + +.numeric-keypad .keypad-key { + width: calc((100% - 20px) / 3); + flex: 0 0 calc((100% - 20px) / 3); +} + +.keypad-key { + min-height: 62px; + border: 1px solid var(--line); + border-radius: 8px; + background: #fff; + color: var(--ink); + font-size: 24px; + font-weight: 900; +} + +.keypad-key:active { + border-color: var(--teal); + background: #e9f4f1; +} + +.keypad-key.utility { + font-size: 18px; + color: #3e5559; + background: #f4f7f6; +} + +.password-panel { + margin-top: 14px; + padding-top: 14px; + border-top: 1px solid #eef2f0; +} + +.compact-keypad { + max-width: 420px; +} + +.danger-text { + color: var(--red); + font-weight: 800; +} + +.settings-screen { + background: #f1f5f3; +} + +.settings-layout { + flex: 1; + display: grid; + grid-template-columns: 178px minmax(0, 1fr); + min-height: calc(100vh - 74px); + min-height: calc(100dvh - 74px); +} + +.settings-nav { + padding: 14px; + border-right: 1px solid var(--line); + background: #fff; +} + +.tab-button { + width: 100%; + height: 44px; + min-height: 44px; + flex: 0 0 auto; + display: flex; + align-items: center; + justify-content: flex-start; + margin-bottom: 7px; + border: 0; + border-radius: 8px; + background: transparent; + color: var(--ink); + text-align: left; + font-weight: 800; + line-height: 1.1; + padding: 0 12px; + white-space: nowrap; +} + +.tab-button.active { + background: var(--deep); + color: #fff; +} + +.settings-main { + padding: 18px; + overflow: auto; +} + +.settings-section { + display: grid; + gap: 14px; +} + +.panel { + padding: 16px; + border: 1px solid var(--line); + border-radius: 8px; + background: #fff; +} + +.panel-title { + margin: 0 0 12px; + font-size: 18px; + letter-spacing: 0; +} + +.form-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 14px; +} + +.port-settings-panel { + padding: 12px; +} + +.port-settings-panel .panel-title { + margin-bottom: 8px; +} + +.port-setting-buttons { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 12px; +} + +.port-setting-button { + min-height: 96px; + padding: 14px; + border: 1px solid var(--line); + border-radius: 8px; + background: #fff; + color: var(--ink); + display: grid; + align-content: center; + gap: 7px; + text-align: left; + box-shadow: 0 8px 18px rgba(18, 53, 55, 0.07); +} + +.port-setting-button strong { + font-size: 22px; +} + +.port-setting-button span { + color: var(--muted); + font-size: 16px; + font-weight: 800; +} + +.port-setting-button:active { + border-color: var(--teal); + background: #e9f6f2; +} + +.port-setting-button.disabled, +.port-setting-button:disabled { + background: #f2f4f3; + color: #98a2a3; + box-shadow: none; + cursor: default; +} + +.port-setting-button.disabled span, +.port-setting-button:disabled span { + color: #98a2a3; +} + +.port-settings-backdrop { + position: fixed; + inset: 0; + z-index: 55; + display: flex; + align-items: center; + justify-content: center; + padding: 24px; + background: rgba(10, 28, 30, 0.38); +} + +.port-settings-dialog { + width: min(92vw, 760px); + max-height: min(78vh, 620px); + margin: auto; + overflow: auto; + padding: 18px; + border: 1px solid var(--line); + border-radius: 8px; + background: #fff; + box-shadow: var(--shadow); +} + +.port-settings-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 14px; +} + +.port-settings-head h3 { + margin: 0; + font-size: 24px; +} + +.port-settings-actions { + justify-content: flex-end; + margin-top: 14px; +} + +.port-config-main, +.port-config-readings { + display: grid; + gap: 8px; + align-items: end; +} + +.port-config-main { + grid-template-columns: repeat(4, minmax(0, 1fr)); +} + +.port-config-main .port-type-field { + grid-column: span 2; +} + +.port-config-readings { + grid-template-columns: repeat(4, minmax(0, 1fr)); + margin-top: 8px; +} + +.port-config-main .field, +.port-config-readings .field { + gap: 3px; + margin-bottom: 0; +} + +.port-config-main .field label, +.port-config-readings .field label { + font-size: 12px; +} + +.port-config-main .field input, +.port-config-main .field select, +.port-config-main .readonly-value, +.port-config-readings .field input, +.port-config-readings .field select, +.port-config-readings .readonly-value { + min-height: 38px; + padding: 0 9px; + font-size: 14px; +} + +.port-status-cell { + min-height: 38px; + padding: 6px 9px; + border: 1px solid var(--line); + border-radius: 8px; + background: #fff; + display: grid; + align-content: center; + gap: 2px; +} + +.port-status-cell span { + color: var(--muted); + font-size: 12px; +} + +.port-status-cell strong { + color: var(--ink); + font-size: 15px; +} + +.port-toggle-cell { + min-width: 0; +} + +.port-toggle-cell .toggle-row { + min-height: 38px; + padding: 0; + border-bottom: 0; +} + +.port-toggle-cell .toggle-row span { + font-size: 14px; +} + +.port-toggle-cell .toggle-row small { + display: none; +} + +.port-toggle-cell .small { + margin: 4px 0 0; + font-size: 11px; +} + +.toggle-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + min-height: 44px; + padding: 8px 0; + border-bottom: 1px solid #eef2f0; +} + +.toggle-row:last-child { + border-bottom: 0; +} + +.toggle-row span { + font-weight: 800; +} + +.toggle-row small { + display: block; + margin-top: 3px; + color: var(--muted); +} + +.switch { + position: relative; + width: 54px; + min-width: 54px; + height: 30px; +} + +.switch input { + opacity: 0; + width: 0; + height: 0; +} + +.slider { + position: absolute; + inset: 0; + border-radius: 30px; + background: #b9c4c0; + transition: 0.18s; +} + +.slider::before { + content: ""; + position: absolute; + width: 24px; + height: 24px; + left: 3px; + top: 3px; + border-radius: 50%; + background: #fff; + transition: 0.18s; +} + +.switch input:checked + .slider { + background: var(--teal); +} + +.switch input:checked + .slider::before { + transform: translateX(24px); +} + +.cache-list { + display: grid; + gap: 8px; + margin-top: 12px; +} + +.cache-item { + display: grid; + grid-template-columns: 1fr auto; + gap: 12px; + padding: 11px; + border: 1px solid var(--line); + border-radius: 8px; + background: #fbfdfc; +} + +.cache-item strong { + display: block; + margin-bottom: 4px; +} + +.small { + color: var(--muted); + font-size: 13px; +} + +.test-results { + display: grid; + gap: 8px; + margin-top: 12px; +} + +.result-line { + display: flex; + justify-content: space-between; + gap: 12px; + padding: 9px 11px; + border-radius: 8px; + background: #f4f7f6; +} + +.result-line.ok strong { + color: var(--green); +} + +.result-line.bad strong { + color: var(--red); +} + +.toast { + position: fixed; + left: 50%; + bottom: 24px; + z-index: 20; + min-width: 220px; + max-width: min(92vw, 520px); + transform: translate(-50%, 140%); + padding: 12px 16px; + border-radius: 8px; + background: rgba(24, 40, 43, 0.94); + color: #fff; + text-align: center; + font-weight: 800; + transition: transform 0.2s ease; +} + +.toast.show { + transform: translate(-50%, 0); +} + +@media (max-width: 720px) { + .topbar { + align-items: flex-start; + flex-direction: column; + } + + .topbar-actions { + justify-content: flex-start; + } + + .settings-screen .topbar { + flex-direction: row; + align-items: center; + } + + .settings-screen .topbar-actions { + margin-left: auto; + justify-content: flex-end; + } + + .home-main, + .content, + .flow-body { + padding: 14px; + } + + .app-log-window { + top: 128px; + right: 8px; + width: calc(100% - 16px); + max-height: 150px; + padding: 8px; + } + + .log-line { + grid-template-columns: 52px 40px minmax(0, 1fr); + } + + .home-lower { + min-height: 420px; + padding: 22px 16px 76px; + } + + .home-camera-preview { + width: calc(100% + 32px); + max-width: none; + height: 420px; + margin-left: -16px; + margin-right: -16px; + } + + .settings-camera-preview { + max-width: 420px; + height: 315px; + } + + .admin-entry { + right: 12px; + bottom: 12px; + } + + .ports-grid, + .form-grid, + .work-summary { + grid-template-columns: 1fr; + } + + .ports-grid.port-count-2, + .ports-grid.port-count-4 { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .ports-grid.port-count-3 { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + + .success-port-result-grid.port-count-2, + .success-port-result-grid.port-count-3, + .success-port-result-grid.port-count-4 { + grid-template-columns: 1fr; + } + + .port-setting-buttons { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .port-settings-dialog { + width: min(94vw, 620px); + } + + .port-config-main, + .port-config-readings { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .port-config-main .port-type-field { + grid-column: span 2; + } + + .settings-layout { + grid-template-columns: 1fr; + grid-template-rows: auto minmax(0, 1fr); + align-content: start; + } + + .settings-nav { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + align-items: center; + align-self: start; + gap: 8px; + height: auto; + min-height: 0; + border-right: 0; + border-bottom: 1px solid var(--line); + } + + .tab-button { + width: 100%; + min-width: 0; + margin-bottom: 0; + justify-content: center; + text-align: center; + font-size: 15px; + padding: 0 6px; + } + + .user-band, + .cache-item { + grid-template-columns: 1fr; + } +} + +@media (orientation: portrait) { + .app-shell { + width: 100vw; + } + + .topbar, + .settings-screen .topbar { + min-height: 68px; + padding: 10px 14px; + flex-flow: row wrap; + align-items: center; + } + + .brand-mark { + width: 38px; + height: 38px; + } + + .brand-title { + font-size: 19px; + } + + .topbar-actions { + margin-left: auto; + } + + .content, + .home-main, + .flow-body { + padding: 12px; + } + + .boot-panel, + .panel, + .login-box { + padding: 16px; + } + + .home-lower { + min-height: calc(100dvh - 92px); + padding: 22px 14px 76px; + } + + .prompt-title { + font-size: clamp(26px, 7vw, 38px); + } + + .prompt-subtitle { + margin-top: 10px; + font-size: 15px; + } + + .home-camera-preview { + width: 100%; + max-width: 620px; + margin: 14px auto 0; + } + + .face-recognition-indicator { + margin-top: 14px; + font-size: 17px; + } + + .home-actions { + justify-content: center; + } + + .home-actions .btn { + flex: 1 1 130px; + } + + .ports-grid.port-count-3, + .ports-grid.port-count-4 { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .port-card, + .ports-grid.compact .port-card { + min-height: 126px; + padding: 13px; + } + + .port-head { + align-items: flex-start; + flex-direction: column; + } + + .delivery-stage { + min-height: calc(100dvh - 92px); + } + + .delivery-actions .btn { + flex: 1 1 135px; + } + + .success-port-result-grid.port-count-2, + .success-port-result-grid.port-count-3, + .success-port-result-grid.port-count-4 { + grid-template-columns: 1fr; + } + + .settings-layout { + grid-template-columns: 1fr; + grid-template-rows: auto minmax(0, 1fr); + } + + .settings-nav { + position: sticky; + top: 0; + z-index: 10; + display: flex; + gap: 7px; + overflow-x: auto; + padding: 9px 10px; + border-right: 0; + border-bottom: 1px solid var(--line); + scrollbar-width: none; + } + + .settings-nav::-webkit-scrollbar { + display: none; + } + + .tab-button { + width: auto; + min-width: 92px; + margin: 0; + justify-content: center; + padding: 0 12px; + } + + .settings-main { + padding: 12px; + } + + .face-settings-section, + .form-grid { + grid-template-columns: 1fr; + } + + .settings-camera-preview { + width: 100%; + max-width: 620px; + height: 420px; + } + + .face-people-head { + align-items: stretch; + flex-direction: column; + } + + .face-people-table { + min-width: 620px; + } +} diff --git a/app/src/main/cpp/face_engine_jni.cpp b/app/src/main/cpp/face_engine_jni.cpp new file mode 100644 index 0000000..b60813a --- /dev/null +++ b/app/src/main/cpp/face_engine_jni.cpp @@ -0,0 +1,315 @@ +#include + +#include +#include +#include +#include +#include +#include + +#include "seeta/FaceDetector.h" +#include "seeta/FaceLandmarker.h" +#include "seeta/FaceRecognizer.h" + +namespace { + +std::mutex g_mutex; +std::unique_ptr g_detector; +std::unique_ptr g_landmarker; +std::unique_ptr g_recognizer; +std::string g_last_error; + +constexpr double kCenterLeftRatio = 0.25; +constexpr double kCenterRightRatio = 0.75; +constexpr double kCenterTopRatio = 0.18; +constexpr double kCenterBottomRatio = 0.82; +constexpr double kMaxEyeTiltRatio = 0.18; +constexpr double kMaxMouthTiltRatio = 0.22; +constexpr double kMaxNoseOffsetRatio = 0.20; +constexpr double kMinSymmetryRatio = 0.62; +constexpr double kMinSharpnessVariance = 45.0; + +std::string to_string(JNIEnv *env, jstring value) { + if (value == nullptr) return std::string(); + const char *characters = env->GetStringUTFChars(value, nullptr); + if (characters == nullptr) return std::string(); + std::string result(characters); + env->ReleaseStringUTFChars(value, characters); + return result; +} + +unsigned char clamp_color(int value) { + return static_cast(std::max(0, std::min(255, value))); +} + +void nv21_to_bgr(const unsigned char *source, + int width, + int height, + int rotation, + bool mirror, + std::vector &output, + int &output_width, + int &output_height) { + rotation = ((rotation % 360) + 360) % 360; + if (rotation != 90 && rotation != 180 && rotation != 270) rotation = 0; + output_width = (rotation == 90 || rotation == 270) ? height : width; + output_height = (rotation == 90 || rotation == 270) ? width : height; + output.assign(static_cast(output_width) * output_height * 3, 0); + const int frame_size = width * height; + + for (int sy = 0; sy < height; ++sy) { + const int uv_row = frame_size + (sy >> 1) * width; + for (int sx = 0; sx < width; ++sx) { + int y = static_cast(source[sy * width + sx]) & 0xff; + int uv_index = uv_row + (sx & ~1); + int v = (static_cast(source[uv_index]) & 0xff) - 128; + int u = (static_cast(source[uv_index + 1]) & 0xff) - 128; + int c = std::max(0, y - 16); + int red = (298 * c + 409 * v + 128) >> 8; + int green = (298 * c - 100 * u - 208 * v + 128) >> 8; + int blue = (298 * c + 516 * u + 128) >> 8; + + int rx = sx; + int ry = sy; + if (rotation == 90) { + rx = height - 1 - sy; + ry = sx; + } else if (rotation == 180) { + rx = width - 1 - sx; + ry = height - 1 - sy; + } else if (rotation == 270) { + rx = sy; + ry = width - 1 - sx; + } + if (mirror) rx = output_width - 1 - rx; + size_t index = (static_cast(ry) * output_width + rx) * 3; + output[index] = clamp_color(blue); + output[index + 1] = clamp_color(green); + output[index + 2] = clamp_color(red); + } + } +} + +bool face_center_is_in_capture_area(const SeetaRect &face, int image_width, int image_height) { + const double center_x = face.x + face.width * 0.5; + const double center_y = face.y + face.height * 0.5; + return center_x >= image_width * kCenterLeftRatio + && center_x <= image_width * kCenterRightRatio + && center_y >= image_height * kCenterTopRatio + && center_y <= image_height * kCenterBottomRatio; +} + +double point_distance(const SeetaPointF &first, const SeetaPointF &second) { + const double dx = first.x - second.x; + const double dy = first.y - second.y; + return std::sqrt(dx * dx + dy * dy); +} + +double symmetry_ratio(double first, double second) { + const double largest = std::max(first, second); + return largest <= 0.0001 ? 0.0 : std::min(first, second) / largest; +} + +bool landmarks_are_frontal(const std::vector &points) { + if (points.size() < 5) return false; + const SeetaPointF &left_eye = points[0]; + const SeetaPointF &right_eye = points[1]; + const SeetaPointF &nose = points[2]; + const SeetaPointF &left_mouth = points[3]; + const SeetaPointF &right_mouth = points[4]; + const double eye_width = std::fabs(right_eye.x - left_eye.x); + if (eye_width < 1.0) return false; + if (std::fabs(right_eye.y - left_eye.y) / eye_width > kMaxEyeTiltRatio) return false; + const double mouth_width = std::fabs(right_mouth.x - left_mouth.x); + if (mouth_width < 1.0 + || std::fabs(right_mouth.y - left_mouth.y) / mouth_width > kMaxMouthTiltRatio) { + return false; + } + const double eye_center_x = (left_eye.x + right_eye.x) * 0.5; + const double eye_center_y = (left_eye.y + right_eye.y) * 0.5; + const double mouth_center_x = (left_mouth.x + right_mouth.x) * 0.5; + const double mouth_center_y = (left_mouth.y + right_mouth.y) * 0.5; + if (std::fabs(nose.x - eye_center_x) / eye_width > kMaxNoseOffsetRatio) return false; + if (std::fabs(mouth_center_x - nose.x) / eye_width > kMaxNoseOffsetRatio) return false; + if (nose.y <= eye_center_y || mouth_center_y <= nose.y) return false; + if (symmetry_ratio(point_distance(nose, left_eye), point_distance(nose, right_eye)) + < kMinSymmetryRatio) { + return false; + } + return symmetry_ratio(point_distance(nose, left_mouth), point_distance(nose, right_mouth)) + >= kMinSymmetryRatio; +} + +int gray_at(const unsigned char *bgr, int width, int x, int y) { + const size_t index = (static_cast(y) * width + x) * 3; + return (29 * bgr[index] + 150 * bgr[index + 1] + 77 * bgr[index + 2]) >> 8; +} + +double face_sharpness_variance(const unsigned char *bgr, + int image_width, + int image_height, + const SeetaRect &face) { + const int inset_x = std::max(2, face.width / 10); + const int inset_y = std::max(2, face.height / 10); + const int left = std::max(1, face.x + inset_x); + const int top = std::max(1, face.y + inset_y); + const int right = std::min(image_width - 2, face.x + face.width - inset_x); + const int bottom = std::min(image_height - 2, face.y + face.height - inset_y); + if (right <= left || bottom <= top) return 0.0; + double sum = 0.0; + double square_sum = 0.0; + long count = 0; + for (int y = top; y <= bottom; y += 2) { + for (int x = left; x <= right; x += 2) { + const int center = gray_at(bgr, image_width, x, y); + const int laplacian = 4 * center + - gray_at(bgr, image_width, x - 1, y) + - gray_at(bgr, image_width, x + 1, y) + - gray_at(bgr, image_width, x, y - 1) + - gray_at(bgr, image_width, x, y + 1); + sum += laplacian; + square_sum += static_cast(laplacian) * laplacian; + ++count; + } + } + if (count <= 0) return 0.0; + const double mean = sum / count; + return square_sum / count - mean * mean; +} + +} // namespace + +extern "C" JNIEXPORT jboolean JNICALL +Java_com_xingyuan_zhiling_SeetaFaceNative_initialize( + JNIEnv *env, jclass, jstring detector_model, jstring landmarker_model, jstring recognizer_model) { + std::lock_guard lock(g_mutex); + try { + g_recognizer.reset(); + g_landmarker.reset(); + g_detector.reset(); + std::string detector_path = to_string(env, detector_model); + std::string landmarker_path = to_string(env, landmarker_model); + std::string recognizer_path = to_string(env, recognizer_model); + if (detector_path.empty() || landmarker_path.empty() || recognizer_path.empty()) { + g_last_error = "模型路径不能为空"; + return JNI_FALSE; + } + g_detector.reset(new seeta::FaceDetector(seeta::ModelSetting(detector_path, seeta::ModelSetting::CPU))); + g_landmarker.reset(new seeta::FaceLandmarker(seeta::ModelSetting(landmarker_path, seeta::ModelSetting::CPU))); + g_recognizer.reset(new seeta::FaceRecognizer(seeta::ModelSetting(recognizer_path, seeta::ModelSetting::CPU))); + g_detector->set(seeta::FaceDetector::PROPERTY_MIN_FACE_SIZE, 80); + g_detector->set(seeta::FaceDetector::PROPERTY_NUMBER_THREADS, 2); + g_recognizer->set(seeta::FaceRecognizer::PROPERTY_NUMBER_THREADS, 2); + g_last_error.clear(); + return JNI_TRUE; + } catch (const std::exception &error) { + g_last_error = error.what(); + } catch (...) { + g_last_error = "SeetaFace6 初始化发生未知错误"; + } + g_recognizer.reset(); + g_landmarker.reset(); + g_detector.reset(); + return JNI_FALSE; +} + +extern "C" JNIEXPORT jfloatArray JNICALL +Java_com_xingyuan_zhiling_SeetaFaceNative_extractFeature( + JNIEnv *env, jclass, jbyteArray nv21, jint width, jint height, jint rotation, jboolean mirror) { + if (nv21 == nullptr || width <= 0 || height <= 0) return nullptr; + jsize length = env->GetArrayLength(nv21); + if (length < width * height * 3 / 2) return nullptr; + std::vector source(static_cast(length)); + env->GetByteArrayRegion(nv21, 0, length, reinterpret_cast(source.data())); + + std::vector bgr; + int image_width = 0; + int image_height = 0; + nv21_to_bgr(source.data(), width, height, rotation, mirror == JNI_TRUE, bgr, image_width, image_height); + + std::lock_guard lock(g_mutex); + if (!g_detector || !g_landmarker || !g_recognizer) { + g_last_error = "识别引擎未就绪"; + return nullptr; + } + try { + SeetaImageData image = {image_width, image_height, 3, bgr.data()}; + SeetaFaceInfoArray faces = g_detector->detect(image); + if (faces.size <= 0 || faces.data == nullptr) { + g_last_error = "画面中未检测到正面人脸"; + return nullptr; + } + int selected = -1; + long largest_area = 0; + for (int i = 0; i < faces.size; ++i) { + if (!face_center_is_in_capture_area(faces.data[i].pos, image_width, image_height)) { + continue; + } + long area = static_cast(faces.data[i].pos.width) * faces.data[i].pos.height; + if (area > largest_area) { + largest_area = area; + selected = i; + } + } + if (selected < 0) { + g_last_error = "请将人脸移至取景框中央"; + return nullptr; + } + SeetaRect face = faces.data[selected].pos; + if (face.width < 80 || face.height < 80) { + g_last_error = "人脸距离较远,请靠近摄像头"; + return nullptr; + } + int point_count = g_landmarker->number(); + if (point_count < 5) { + g_last_error = "人脸关键点模型不可用"; + return nullptr; + } + std::vector points(static_cast(point_count)); + g_landmarker->mark(image, face, points.data()); + if (!landmarks_are_frontal(points)) { + g_last_error = "请正对摄像头,侧脸不进行识别"; + return nullptr; + } + if (face_sharpness_variance(bgr.data(), image_width, image_height, face) + < kMinSharpnessVariance) { + g_last_error = "画面模糊,请站稳并保持静止"; + return nullptr; + } + int feature_size = g_recognizer->GetExtractFeatureSize(); + if (feature_size <= 0) { + g_last_error = "人脸特征模型不可用"; + return nullptr; + } + std::vector feature(static_cast(feature_size)); + if (!g_recognizer->Extract(image, points.data(), feature.data())) { + g_last_error = "人脸特征提取失败,请保持正脸和充足光线"; + return nullptr; + } + jfloatArray result = env->NewFloatArray(feature_size); + if (result == nullptr) return nullptr; + env->SetFloatArrayRegion(result, 0, feature_size, feature.data()); + g_last_error.clear(); + return result; + } catch (const std::exception &error) { + g_last_error = error.what(); + } catch (...) { + g_last_error = "SeetaFace6 特征提取发生未知错误"; + } + return nullptr; +} + +extern "C" JNIEXPORT jstring JNICALL +Java_com_xingyuan_zhiling_SeetaFaceNative_lastError(JNIEnv *env, jclass) { + std::lock_guard lock(g_mutex); + return env->NewStringUTF(g_last_error.c_str()); +} + +extern "C" JNIEXPORT void JNICALL +Java_com_xingyuan_zhiling_SeetaFaceNative_release(JNIEnv *, jclass) { + std::lock_guard lock(g_mutex); + g_recognizer.reset(); + g_landmarker.reset(); + g_detector.reset(); + g_last_error.clear(); +} diff --git a/app/src/main/cpp/include/seeta/CFaceInfo.h b/app/src/main/cpp/include/seeta/CFaceInfo.h new file mode 100644 index 0000000..3605696 --- /dev/null +++ b/app/src/main/cpp/include/seeta/CFaceInfo.h @@ -0,0 +1,23 @@ +#pragma once + +#include "Common/CStruct.h" + +#ifdef __cplusplus +extern "C" { +#endif + +struct SeetaFaceInfo +{ + SeetaRect pos; + float score; +}; + +struct SeetaFaceInfoArray +{ + struct SeetaFaceInfo *data; + int size; +}; + +#ifdef __cplusplus +} +#endif diff --git a/app/src/main/cpp/include/seeta/Common/CStruct.h b/app/src/main/cpp/include/seeta/Common/CStruct.h new file mode 100644 index 0000000..4c3c8ed --- /dev/null +++ b/app/src/main/cpp/include/seeta/Common/CStruct.h @@ -0,0 +1,97 @@ +#ifndef INC_SEETA_C_STRUCT_H +#define INC_SEETA_C_STRUCT_H + +#ifdef _MSC_VER +#ifdef SEETA_EXPORTS +#define SEETA_API __declspec(dllexport) +#else +#define SEETA_API __declspec(dllimport) +#endif +#else +#define SEETA_API __attribute__ ((visibility("default"))) +#endif + +#define SEETA_C_API extern "C" SEETA_API + +#define INCLUDED_SEETA_CSTRUCT + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +struct SeetaImageData +{ + int width; + int height; + int channels; + unsigned char *data; +}; + +struct SeetaPoint +{ + int x; + int y; +}; + +struct SeetaPointF +{ + double x; + double y; +}; + +struct SeetaSize +{ + int width; + int height; +}; + +struct SeetaRect +{ + int x; + int y; + int width; + int height; +}; + +struct SeetaRegion +{ + int top; + int bottom; + int left; + int right; +}; + +enum SeetaDevice +{ + SEETA_DEVICE_AUTO = 0, + SEETA_DEVICE_CPU = 1, + SEETA_DEVICE_GPU = 2, +}; + +struct SeetaModelSetting +{ + enum SeetaDevice device; + int id; // when device is GPU, id means GPU id + const char **model; // model string terminate with nullptr +}; + +struct SeetaBuffer +{ + void *buffer; + int64_t size; +}; + +struct SeetaModelBuffer +{ + enum SeetaDevice device; + int id; // when device is GPU, id means GPU id + const SeetaBuffer *buffer; // input buffers, terminate with empty buffer(buffer=nullptr, size=0) +}; + +#ifdef __cplusplus +} +#endif + +#endif // INC_SEETA_C_STRUCT_H diff --git a/app/src/main/cpp/include/seeta/Common/Struct.h b/app/src/main/cpp/include/seeta/Common/Struct.h new file mode 100644 index 0000000..7d776da --- /dev/null +++ b/app/src/main/cpp/include/seeta/Common/Struct.h @@ -0,0 +1,703 @@ +#ifndef INC_SEETA_STRUCT_H +#define INC_SEETA_STRUCT_H + +#include "CStruct.h" + +#include +#include +#include +#include +#include +#include + +#define INCLUDED_SEETA_STRUCT + +namespace seeta +{ + class ImageData : public SeetaImageData + { + public: + using self = ImageData; + using supper = SeetaImageData; + + using byte = unsigned char; + + ~ImageData() = default; + + ImageData(const supper &image) + : ImageData(image.data, image.width, image.height, image.channels) {} + + ImageData(int width, int height, int channels) + : supper({ width, height, channels, nullptr }) + { + this->m_data.reset(new byte[this->count()], std::default_delete()); + this->data = this->m_data.get(); + } + + ImageData() : ImageData(0, 0, 0) {} + + ImageData(const byte *data, int width, int height, int channels) + : ImageData(width, height, channels) + { + this->copy_from(data); + } + + ImageData(const self&) = default; + + ImageData &operator=(const self &) = default; + + ImageData &operator=(const supper &other) + { + this->operator=(self(other)); + return *this; + } + + ImageData(self &&other) + : supper({ other.width, other.height, other.channels, nullptr }) + { + this->m_data = std::move(other.m_data); + this->data = this->m_data.get(); + } + + ImageData &operator=(self &&other) + { + this->width = other.width; + this->height = other.height; + this->channels = other.channels; + this->m_data = std::move(other.m_data); + this->data = this->m_data.get(); + return *this; + } + + void copy_from(const byte *data, int size = -1) + { + int copy_size = this->count(); + copy_size = size < 0 ? copy_size : std::min(copy_size, size); + copy(this->data, data, copy_size); + } + + void copy_to(byte *data, int size = -1) const + { + int copy_size = this->count(); + copy_size = size < 0 ? copy_size : std::min(copy_size, size); + copy(data, this->data, copy_size); + } + + static void copy(byte *dst, const byte *src, size_t size) + { + std::memcpy(dst, src, size); + } + + int count() const { return this->width * this->height * this->channels; } + + ImageData clone() const + { + return ImageData(this->data, this->width, this->height, this->channels); + } + + private: + std::shared_ptr m_data; + }; + + class Point : public SeetaPoint + { + public: + using self = Point; + using supper = SeetaPoint; + + Point(const supper &other) : supper(other) {} + + Point(int x, int y) : supper({ x, y }) {} + + Point() : Point(0, 0) {} + }; + + class PointF : public SeetaPointF + { + public: + using self = PointF; + using supper = SeetaPointF; + + PointF(const supper &other) : supper(other) {} + + PointF(double x, double y) : supper({ x, y }) {} + + PointF() : PointF(0, 0) {} + }; + + class Size : public SeetaSize + { + public: + using self = Size; + using supper = SeetaSize; + + Size(const supper &other) : supper(other) {} + + Size(int width, int height) : supper({ width, height }) {} + + Size() : Size(0, 0) {} + }; + + class Rect : public SeetaRect + { + public: + using self = Rect; + using supper = SeetaRect; + + Rect(const supper &other) : supper(other) {} + + Rect(int x, int y, int width, int height) : supper({ x, y, width, height }) {} + + Rect() : Rect(0, 0, 0, 0) {} + + Rect(int x, int y, const Size &size) : supper({ x, y, size.width, size.height }) {} + + Rect(const Point &top_left, int width, int height) : supper({ top_left.x, top_left.y, width, height }) {} + + Rect(const Point &top_left, const Size &size) : supper({ top_left.x, top_left.y, size.width, size.height }) {} + + Rect(const Point &top_left, const Point &bottom_right) : supper({ top_left.x, top_left.y, bottom_right.x - top_left.x, bottom_right.y - top_left.y }) {} + + operator Point() const { return{ this->x, this->y }; } + + operator Size() const { return{ this->width, this->height }; } + }; + + class Region : public SeetaRegion + { + public: + using self = Region; + using supper = SeetaRegion; + + Region(const supper &other) : supper(other) {} + + Region(int top, int bottom, int left, int right) : supper({ top, bottom, left, right }) {} + + Region() : Region(0, 0, 0, 0) {} + + Region(const Rect &rect) : Region(rect.y, rect.y + rect.height, rect.x, rect.x + rect.width) {} + + operator Rect() const { return{ left, top, right - left, bottom - top }; } + }; + + class ModelSetting : public SeetaModelSetting + { + public: + using self = ModelSetting; + using supper = SeetaModelSetting; + + enum Device + { + AUTO, + CPU, + GPU + }; + + ~ModelSetting() = default; + + ModelSetting() + : supper({ SEETA_DEVICE_AUTO, 0, nullptr }) + { + this->update(); + } + + ModelSetting(const supper &other) + : supper({ other.device, other.id, nullptr }) + { + if (other.model) { + int i = 0; + while (other.model[i]) + { + m_model_string.emplace_back(other.model[i]); + ++i; + } + } + this->update(); + } + + ModelSetting(const self &other) + : supper({ other.device, other.id, nullptr }) + { + this->m_model_string = other.m_model_string; + this->update(); + } + + ModelSetting &operator=(const supper &other) + { + this->operator=(self(other)); + return *this; + } + + ModelSetting &operator=(const self &other) + { + this->device = other.device; + this->id = other.id; + this->m_model_string = other.m_model_string; + this->update(); + return *this; + } + + ModelSetting(self &&other) + : supper({ other.device, other.id, nullptr }) + { + this->m_model_string = std::move(other.m_model_string); + this->update(); + } + + ModelSetting &operator=(self &&other) + { + this->device = other.device; + this->id = other.id; + this->m_model_string = std::move(other.m_model_string); + this->update(); + return *this; + } + + ModelSetting(const std::string &model, SeetaDevice device, int id) + : supper({ device, id, nullptr }) + { + this->append(model); + } + + ModelSetting(const std::string &model, SeetaDevice device) : self(model, device, 0) {} + + ModelSetting(const std::string &model, Device device, int id) : self(model, SeetaDevice(device), id) {} + + ModelSetting(const std::string &model, Device device) : self(model, SeetaDevice(device)) {} + + ModelSetting(const std::string &model) : self(model, SEETA_DEVICE_AUTO) {} + + ModelSetting(const std::vector &model, SeetaDevice device, int id) + : supper({ device, id, nullptr }) + { + this->append(model); + } + + ModelSetting(const std::vector &model, SeetaDevice device) : self(model, device, 0) {} + + ModelSetting(const std::vector &model, Device device, int id) : self(model, SeetaDevice(device), id) {} + + ModelSetting(const std::vector &model, Device device) : self(model, SeetaDevice(device)) {} + + ModelSetting(const std::vector &model) : self(model, SEETA_DEVICE_AUTO) {} + + ModelSetting(SeetaDevice device, int id) + : supper({ device, id, nullptr }) + { + this->update(); + } + + ModelSetting(SeetaDevice device) : self(device, 0) {} + + ModelSetting(Device device, int id) : self(SeetaDevice(device), id) {} + + ModelSetting(Device device) : self(SeetaDevice(device)) {} + + Device get_device() const { return Device(this->device); } + int get_id() const { return this->id; } + + Device set_device(Device device) + { + return set_device(SeetaDevice(device)); + } + + Device set_device(SeetaDevice device) + { + auto old = this->device; + this->device = device; + return Device(old); + } + + int set_id(int id) + { + const auto old = this->id; + this->id = id; + return old; + } + + void clear() + { + this->m_model_string.clear(); + this->update(); + } + + void append(const std::string &model) + { + this->m_model_string.push_back(model); + this->update(); + } + + void append(const std::vector &model) + { + this->m_model_string.insert(this->m_model_string.end(), model.begin(), model.end()); + this->update(); + } + + const std::vector &get_model() const + { + return this->m_model_string; + } + + const std::string &get_model(size_t i) const + { + return this->m_model_string[i]; + } + + size_t count() const + { + return this->m_model_string.size(); + } + + private: + std::vector m_model; + std::vector m_model_string; + + /** + * \brief build buffer::model + */ + void update() + { + m_model.clear(); + m_model.reserve(m_model_string.size() + 1); + for (auto &model_string : m_model_string) + { + m_model.push_back(model_string.c_str()); + } + m_model.push_back(nullptr); + this->model = m_model.data(); + } + }; + + class Buffer : public SeetaBuffer + { + public: + using self = Buffer; + using supper = SeetaBuffer; + + using byte = unsigned char; + + ~Buffer() = default; + + Buffer(const supper &other) : Buffer(other.buffer, other.size) {} + + Buffer(const self &) = default; + + Buffer &operator=(const self &) = default; + + Buffer &operator=(const supper &other) + { + this->operator=(self(other)); + return *this; + } + + Buffer(self &&other) + : supper({ other.buffer, other.size }), m_buffer(std::move(other.m_buffer)), m_size(other.m_size) {} + + Buffer &operator=(self &&other) + { + this->buffer = other.buffer; + this->size = other.size; + this->m_buffer = std::move(other.m_buffer); + this->m_size = other.m_size; + return *this; + } + + /** + * \brief contruct cpp-style-buffer from c-style-buffer, optional borrow or copy + * \param other c-style-buffer + * \param borrow if borrow or copy buffer + */ + Buffer(const supper &other, bool borrow) + : supper({ nullptr, 0 }) + { + if (borrow) this->borrow(other.buffer, other.size); + else this->rebind(other.buffer, other.size); + } + + Buffer(const void *buffer, int64_t size) + : supper({ nullptr, 0 }) + { + this->m_size = size; + if (this->m_size) + { + this->m_buffer.reset(new byte[size_t(this->m_size)], std::default_delete()); + } + this->buffer = this->m_buffer.get(); + this->size = this->m_size; + if (buffer != nullptr) + { + this->copy_from(buffer, size); + } + } + + explicit Buffer(int64_t size) : Buffer(nullptr, size) {} + + Buffer() : Buffer(nullptr, 0) {} + + Buffer(std::istream &in, int64_t size = -1) + : supper({ nullptr, 0 }) + { + if (size < 0) + { + const auto cur = in.tellg(); + in.seekg(0, std::ios::end); + const auto end = in.tellg(); + size = int64_t(end - cur); + in.seekg(-size, std::ios::end); + } + + this->m_size = size; + this->m_buffer.reset(new byte[size_t(this->m_size)], std::default_delete()); + this->buffer = this->m_buffer.get(); + this->size = this->m_size; + + in.read(reinterpret_cast(this->buffer), this->size); + } + + void copy_from(const void *data, int64_t size = -1) + { + auto copy_size = this->m_size; + copy_size = size < 0 ? copy_size : std::min(copy_size, size); + copy(this->buffer, data, size_t(copy_size)); + } + + void copy_to(byte *data, int64_t size = -1) const + { + auto copy_size = this->m_size; + copy_size = size < 0 ? copy_size : std::min(copy_size, size); + copy(data, this->buffer, size_t(copy_size)); + } + + static void copy(void *dst, const void *src, size_t size) + { + if (dst == nullptr || src == nullptr) return; + std::memcpy(dst, src, size); + } + + Buffer clone() const + { + return self(this->buffer, this->size); + } + + void rebind(const void *buffer, int64_t size) + { + if (size < 0) size = 0; + if (size > this->m_size) + { + this->m_buffer.reset(new byte[size_t(size)], std::default_delete()); + } + this->m_size = size; + this->buffer = this->m_buffer.get(); + this->size = this->m_size; + this->copy_from(buffer, size); + } + + /** + * \brief borrow c-style buffer + * \param buffer pointert to buffer + * \param size size of buffer + */ + void borrow(void *buffer, int64_t size) + { + this->m_size = 0; + this->m_buffer.reset(); + this->buffer = buffer; + this->size = size; + } + + private: + std::shared_ptr m_buffer; + int64_t m_size = 0; + }; + + class ModelBuffer : SeetaModelBuffer + { + public: + using self = ModelBuffer; + using supper = SeetaModelBuffer; + + enum Device + { + AUTO = SEETA_DEVICE_AUTO, + CPU = SEETA_DEVICE_CPU, + GPU = SEETA_DEVICE_GPU, + }; + + ~ModelBuffer() = default; + + ModelBuffer() + : supper({ SEETA_DEVICE_AUTO, 0, nullptr }) + { + this->update(); + } + + ModelBuffer(const supper &other) + : supper({ other.device, other.id, nullptr }) + { + if (other.buffer) { + int i = 0; + while (other.buffer[i].buffer && other.buffer[i].size) + { + m_model_buffer.emplace_back(other.buffer[i]); + ++i; + } + } + this->update(); + } + + ModelBuffer(const self &other) + : supper({ other.device, other.id, nullptr }) + { + this->m_model_buffer = other.m_model_buffer; + this->update(); + } + + ModelBuffer &operator=(const supper &other) + { + this->operator=(self(other)); + return *this; + } + + ModelBuffer &operator=(const self &other) + { + this->device = other.device; + this->id = other.id; + this->m_model_buffer = other.m_model_buffer; + this->update(); + return *this; + } + + ModelBuffer(self &&other) + : supper({ other.device, other.id, nullptr }) + { + this->m_model_buffer = std::move(other.m_model_buffer); + this->update(); + } + + ModelBuffer &operator=(self &&other) + { + this->device = other.device; + this->id = other.id; + this->m_model_buffer = std::move(other.m_model_buffer); + this->update(); + return *this; + } + + + ModelBuffer(const seeta::Buffer &buffer, SeetaDevice device, int id) + : supper({ device, id, nullptr }) + { + this->append(buffer); + } + + ModelBuffer(const seeta::Buffer &buffer, SeetaDevice device) : self(buffer, device, 0) {} + + ModelBuffer(const seeta::Buffer &buffer, Device device, int id) : self(buffer, SeetaDevice(device), id) {} + + ModelBuffer(const seeta::Buffer &buffer, Device device) : self(buffer, SeetaDevice(device)) {} + + ModelBuffer(const seeta::Buffer &buffer) : self(buffer, SEETA_DEVICE_AUTO) {} + + ModelBuffer(const std::vector &buffer, SeetaDevice device, int id) + : supper({ device, id, nullptr }) + { + this->append(buffer); + } + + ModelBuffer(const std::vector &buffer, SeetaDevice device) : self(buffer, device, 0) {} + + ModelBuffer(const std::vector &buffer, Device device, int id) : self(buffer, SeetaDevice(device), id) {} + + ModelBuffer(const std::vector &buffer, Device device) : self(buffer, SeetaDevice(device)) {} + + ModelBuffer(const std::vector &buffer) : self(buffer, SEETA_DEVICE_AUTO) {} + + ModelBuffer(SeetaDevice device, int id) + : supper({ device, id, nullptr }) + { + this->update(); + } + + ModelBuffer(SeetaDevice device) : self(device, 0) {} + + ModelBuffer(Device device, int id) : self(SeetaDevice(device), id) {} + + ModelBuffer(Device device) : self(SeetaDevice(device)) {} + + Device get_device() const { return Device(this->device); } + int get_id() const { return this->id; } + + Device set_device(Device device) + { + return set_device(SeetaDevice(device)); + } + + Device set_device(SeetaDevice device) + { + auto old = this->device; + this->device = device; + return Device(old); + } + + int set_id(int id) + { + const auto old = this->id; + this->id = id; + return old; + } + + void clear() + { + this->m_model_buffer.clear(); + this->update(); + } + + void append(const seeta::Buffer &buffer) + { + this->m_model_buffer.push_back(buffer); + this->update(); + } + + void append(const std::vector &model) + { + this->m_model_buffer.insert(this->m_model_buffer.end(), model.begin(), model.end()); + this->update(); + } + + const std::vector &get_buffer() const + { + return this->m_model_buffer; + } + + const seeta::Buffer &get_buffer(size_t i) const + { + return this->m_model_buffer[i]; + } + + size_t count() const + { + return this->m_model_buffer.size(); + } + + private: + std::vector m_buffer; + std::vector m_model_buffer; + + /** + * \brief build supper::buffer + */ + void update() + { + this->m_buffer.clear(); + this->m_buffer.reserve(m_model_buffer.size() + 1); + for (auto &model_buffer : m_model_buffer) + { + this->m_buffer.push_back(model_buffer); + } + this->m_buffer.push_back({ nullptr, 0 }); // terminate with empty buffer + this->buffer = this->m_buffer.data(); + } + }; +} + +#endif diff --git a/app/src/main/cpp/include/seeta/FaceDetector.h b/app/src/main/cpp/include/seeta/FaceDetector.h new file mode 100644 index 0000000..d5d65e0 --- /dev/null +++ b/app/src/main/cpp/include/seeta/FaceDetector.h @@ -0,0 +1,54 @@ +// +// Created by kier on 19-4-24. +// + +#ifndef INC_SEETA_FACEDETECTOR_H +#define INC_SEETA_FACEDETECTOR_H + +#include "seeta/Common/Struct.h" +#include "seeta/CFaceInfo.h" +#include "seeta/SeetaFaceDetectorConfig.h" + +namespace seeta { + namespace SEETA_FACE_DETECTOR_NAMESPACE_VERSION { + class FaceDetector { + public: + using self = FaceDetector; + + enum Property { + PROPERTY_MIN_FACE_SIZE, + PROPERTY_THRESHOLD, + PROPERTY_MAX_IMAGE_WIDTH, + PROPERTY_MAX_IMAGE_HEIGHT, + PROPERTY_NUMBER_THREADS, + + PROPERTY_ARM_CPU_MODE = 0x101, + }; + + SEETA_API explicit FaceDetector(const SeetaModelSetting &setting); + + SEETA_API ~FaceDetector(); + + SEETA_API explicit FaceDetector(const self *other); + + SEETA_API SeetaFaceInfoArray detect(const SeetaImageData &image) const; + + SEETA_API void set(Property property, double value); + + SEETA_API double get(Property property) const; + + private: + FaceDetector(const FaceDetector &) = delete; + + const FaceDetector &operator=(const FaceDetector &) = delete; + + private: + class Implement; + + Implement *m_impl; + }; + } + using namespace SEETA_FACE_DETECTOR_NAMESPACE_VERSION; +} + +#endif //INC_SEETA_FACEDETECTOR_H diff --git a/app/src/main/cpp/include/seeta/FaceLandmarker.h b/app/src/main/cpp/include/seeta/FaceLandmarker.h new file mode 100644 index 0000000..437975a --- /dev/null +++ b/app/src/main/cpp/include/seeta/FaceLandmarker.h @@ -0,0 +1,64 @@ +// +// Created by kier on 19-7-16. +// + +#ifndef SEETA_FACELANDMARKER_FACELANDMARKER_H +#define SEETA_FACELANDMARKER_FACELANDMARKER_H + +#include "Common/Struct.h" +#include "seeta/SeetaFaceLandmarkerConfig.h" + +namespace seeta { + namespace SEETA_FACE_LANDMARKER_NAMESPACE_VERSION { + class FaceLandmarker { + public: + using self = FaceLandmarker; + + SEETA_API explicit FaceLandmarker(const SeetaModelSetting &setting); + SEETA_API ~FaceLandmarker(); + + SEETA_API FaceLandmarker(const self *other); + + SEETA_API int number() const; + + SEETA_API void mark(const SeetaImageData &image, const SeetaRect &face, SeetaPointF *points) const; + + SEETA_API void mark(const SeetaImageData &image, const SeetaRect &face, SeetaPointF *points, int32_t *mask) const; + + std::vector mark(const SeetaImageData &image, const SeetaRect &face) const { + std::vector points(this->number()); + mark(image, face, points.data()); + return points; + } + + class PointWithMask { + public: + SeetaPointF point; + bool mask; + }; + + std::vector mark_v2(const SeetaImageData &image, const SeetaRect &face) const { + std::vector points(this->number()); + std::vector masks(this->number()); + mark(image, face, points.data(), masks.data()); + std::vector point_with_masks(this->number()); + for (int i = 0; i < this->number(); ++i) { + point_with_masks[i].point = points[i]; + point_with_masks[i].mask = masks[i]; + } + return point_with_masks; + } + + private: + FaceLandmarker(const FaceLandmarker &) = delete; + const FaceLandmarker &operator=(const FaceLandmarker&) = delete; + + private: + class Implement; + Implement *m_impl; + }; + } + using namespace SEETA_FACE_LANDMARKER_NAMESPACE_VERSION; +} + +#endif //SEETA_FACELANDMARKER_FACELANDMARKER_H diff --git a/app/src/main/cpp/include/seeta/FaceRecognizer.h b/app/src/main/cpp/include/seeta/FaceRecognizer.h new file mode 100644 index 0000000..6afc310 --- /dev/null +++ b/app/src/main/cpp/include/seeta/FaceRecognizer.h @@ -0,0 +1,81 @@ +// +// Created by kier on 19-4-24. +// + +#ifndef SEETA_FACERECOGNIZER_FACERECOGNIZER_H +#define SEETA_FACERECOGNIZER_FACERECOGNIZER_H + +#include "Common/Struct.h" +#include "seeta/SeetaFaceRecognizerConfig.h" + +// #define SEETA_FACE_RECOGNIZER_MAJOR_VERSION 6 +// #define SEETA_FACE_RECOGNIZER_MINOR_VERSION 0 +// #define SEETA_FACE_RECOGNIZER_SINOR_VERSION 0 + +namespace seeta { + namespace SEETA_FACE_RECOGNIZE_NAMESPACE_VERSION { + class FaceRecognizer { + public: + using self = FaceRecognizer; + enum Property { + PROPERTY_NUMBER_THREADS = 4, + PROPERTY_ARM_CPU_MODE = 5 + }; + + SEETA_API explicit FaceRecognizer(const SeetaModelSetting &setting); + SEETA_API ~FaceRecognizer(); + + SEETA_API FaceRecognizer(const self *other); + + SEETA_API static int GetCropFaceWidth(); + SEETA_API static int GetCropFaceHeight(); + SEETA_API static int GetCropFaceChannels(); + + SEETA_API static bool CropFace(const SeetaImageData &image, const SeetaPointF *points, SeetaImageData &face); + + SEETA_API int GetExtractFeatureSize() const; + + SEETA_API bool ExtractCroppedFace(const SeetaImageData &image, float *features) const; + + SEETA_API bool Extract(const SeetaImageData &image, const SeetaPointF *points, float *features) const; + + SEETA_API float CalculateSimilarity(const float *features1, const float *features2) const; + + static seeta::ImageData CropFace(const SeetaImageData &image, const SeetaPointF *points) { + seeta::ImageData face(GetCropFaceWidth(), GetCropFaceHeight(), GetCropFaceChannels()); + CropFace(image, points, face); + return face; + } + + SEETA_API int GetCropFaceWidthV2() const; + + SEETA_API int GetCropFaceHeightV2() const; + + SEETA_API int GetCropFaceChannelsV2() const; + + SEETA_API bool CropFaceV2(const SeetaImageData &image, const SeetaPointF *points, SeetaImageData &face); + + seeta::ImageData CropFaceV2(const SeetaImageData &image, const SeetaPointF *points) { + seeta::ImageData face(GetCropFaceWidthV2(), GetCropFaceHeightV2(), GetCropFaceChannelsV2()); + CropFaceV2(image, points, face); + return face; + } + + SEETA_API void set(Property property, double value); + + SEETA_API double get(Property property) const; + + + private: + FaceRecognizer(const FaceRecognizer &) = delete; + const FaceRecognizer &operator=(const FaceRecognizer&) = delete; + + private: + class Implement; + Implement *m_impl; + }; + } + using namespace SEETA_FACE_RECOGNIZE_NAMESPACE_VERSION; +} + +#endif //SEETA_FACERECOGNIZER_FACERECOGNIZER_H diff --git a/app/src/main/cpp/include/seeta/SeetaFaceDetectorConfig.h b/app/src/main/cpp/include/seeta/SeetaFaceDetectorConfig.h new file mode 100644 index 0000000..5f75dad --- /dev/null +++ b/app/src/main/cpp/include/seeta/SeetaFaceDetectorConfig.h @@ -0,0 +1,12 @@ +#ifndef _INC_SEETA_FACE_DETECTOR_V6_ +#define _INC_SEETA_FACE_DETECTOR_V6_ + +#define SEETA_FACE_DETECTOR_MAJOR_VERSION 6 +#define SEETA_FACE_DETECTOR_MINOR_VERSION 0 +#define SEETA_FACE_DETECTOR_SINOR_VERSION 0 + +#define SEETA_FACE_DETECTOR_LIBRARY_NAME "SeetaFaceDetector" + +#define SEETA_FACE_DETECTOR_NAMESPACE_VERSION v6 + +#endif // _INC_SEETA_FACE_DETECTOR_V6_ diff --git a/app/src/main/cpp/include/seeta/SeetaFaceLandmarkerConfig.h b/app/src/main/cpp/include/seeta/SeetaFaceLandmarkerConfig.h new file mode 100644 index 0000000..9c19c3d --- /dev/null +++ b/app/src/main/cpp/include/seeta/SeetaFaceLandmarkerConfig.h @@ -0,0 +1,9 @@ +#pragma once + +#define SEETA_FACE_LANDMARKER_MAJOR_VERSION 6 +#define SEETA_FACE_LANDMARKER_MINOR_VERSION 0 +#define SEETA_FACE_LANDMARKER_SINOR_VERSION 0 + +#define SEETA_FACE_LANDMARKER_LIBRARY_NAME "SeetaFaceLandmarker" + +#define SEETA_FACE_LANDMARKER_NAMESPACE_VERSION v6 diff --git a/app/src/main/cpp/include/seeta/SeetaFaceRecognizerConfig.h b/app/src/main/cpp/include/seeta/SeetaFaceRecognizerConfig.h new file mode 100644 index 0000000..981436d --- /dev/null +++ b/app/src/main/cpp/include/seeta/SeetaFaceRecognizerConfig.h @@ -0,0 +1,9 @@ +#pragma once + +#define SEETA_FACE_RECOGNIZER_MAJOR_VERSION 6 +#define SEETA_FACE_RECOGNIZER_MINOR_VERSION 1 +#define SEETA_FACE_RECOGNIZER_SINOR_VERSION 0 + +#define SEETA_FACE_RECOGNIZE_LIBRARY_NAME "SeetaFaceRecognizer" + +#define SEETA_FACE_RECOGNIZE_NAMESPACE_VERSION v6 diff --git a/app/src/main/java/android_serialport_api/SerialPort.java b/app/src/main/java/android_serialport_api/SerialPort.java new file mode 100644 index 0000000..1658188 --- /dev/null +++ b/app/src/main/java/android_serialport_api/SerialPort.java @@ -0,0 +1,67 @@ +/* + * Copyright 2009 Cedric Priscal + * + * Licensed under the Apache License, Version 2.0. + */ + +package android_serialport_api; + +import android.util.Log; + +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class SerialPort { + private static final String TAG = "SerialPort"; + + /* + * Do not remove or rename this field: it is used by native close(). + */ + private FileDescriptor mFd; + private FileInputStream mFileInputStream; + private FileOutputStream mFileOutputStream; + + public SerialPort(File device, int baudrate, int flags) throws SecurityException, IOException { + if (!device.canRead() || !device.canWrite()) { + try { + Process su = Runtime.getRuntime().exec("/system/bin/su"); + String cmd = "chmod 666 " + device.getAbsolutePath() + "\nexit\n"; + su.getOutputStream().write(cmd.getBytes()); + if ((su.waitFor() != 0) || !device.canRead() || !device.canWrite()) { + throw new SecurityException(); + } + } catch (Exception error) { + throw new SecurityException(error); + } + } + + mFd = open(device.getAbsolutePath(), baudrate, flags); + if (mFd == null) { + Log.e(TAG, "native open returns null"); + throw new IOException("native open returns null"); + } + mFileInputStream = new FileInputStream(mFd); + mFileOutputStream = new FileOutputStream(mFd); + } + + public InputStream getInputStream() { + return mFileInputStream; + } + + public OutputStream getOutputStream() { + return mFileOutputStream; + } + + private native static FileDescriptor open(String path, int baudrate, int flags); + + public native void close(); + + static { + System.loadLibrary("serial_port"); + } +} diff --git a/app/src/main/java/com/zhitou/terminal/BootReceiver.java b/app/src/main/java/com/zhitou/terminal/BootReceiver.java new file mode 100644 index 0000000..f5811b2 --- /dev/null +++ b/app/src/main/java/com/zhitou/terminal/BootReceiver.java @@ -0,0 +1,51 @@ +package com.xingyuan.zhiling; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +/** + * 开机自启动接收器。 + * + * 监听 android.intent.action.BOOT_COMPLETED 广播, + * 设备开机完成后自动拉起 MainActivity。 + * + * 注意:该广播只有在以下条件同时满足时才会被接收: + * 1. 应用已安装(非刚安装后首次,需经历过一次重启或手动启动过 app); + * 2. 拥有 RECEIVE_BOOT_COMPLETED 权限。 + * + * 对于嵌入式设备,建议同时将 MainActivity 设为默认桌面(HOME), + * 这样即使 BootReceiver 因系统策略延迟,也能在用户按 Home 键时回到 app。 + */ +public class BootReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + if (intent == null) { + return; + } + + String action = intent.getAction(); + if (action == null) { + return; + } + + // 开机完成 + if (Intent.ACTION_BOOT_COMPLETED.equals(action)) { + launchMainActivity(context); + return; + } + + // 部分定制系统在替换系统包或更新后会发送这些广播 + if ("android.intent.action.QUICKBOOT_POWERON".equals(action) + || "com.android.intent.action.QUICKBOOT_POWERON".equals(action)) { + launchMainActivity(context); + } + } + + private void launchMainActivity(Context context) { + Intent launchIntent = new Intent(context, MainActivity.class); + launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(launchIntent); + } +} diff --git a/app/src/main/java/com/zhitou/terminal/CrashRecovery.java b/app/src/main/java/com/zhitou/terminal/CrashRecovery.java new file mode 100644 index 0000000..b3c5c26 --- /dev/null +++ b/app/src/main/java/com/zhitou/terminal/CrashRecovery.java @@ -0,0 +1,202 @@ +package com.xingyuan.zhiling; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Build; + +import java.io.File; +import java.io.FileWriter; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +final class CrashRecovery { + private static final String PREFS = "xingyuan_crash_recovery"; + private static final String KEY_LAST_CRASH_AT = "last_crash_at"; + private static final String KEY_CRASH_COUNT = "crash_count"; + private static final String KEY_NOTICE = "notice"; + private static final String KEY_STARTUP_CRASH = "startup_crash"; + private static final String KEY_RETRY_GRANTED = "retry_granted"; + private static final long CRASH_WINDOW_MS = 60_000L; + private static final int MAX_RESTARTS_IN_WINDOW = 3; + private static final long RESTART_DELAY_MS = 1_500L; + + private CrashRecovery() { + } + + static void recordAndSchedule(Context context, Throwable error) { + Context app = context.getApplicationContext(); + SharedPreferences preferences = app.getSharedPreferences(PREFS, Context.MODE_PRIVATE); + long now = System.currentTimeMillis(); + long lastCrashAt = preferences.getLong(KEY_LAST_CRASH_AT, 0L); + int count = now - lastCrashAt <= CRASH_WINDOW_MS + ? preferences.getInt(KEY_CRASH_COUNT, 0) + 1 + : 1; + String summary = summarize(error); + preferences.edit() + .putLong(KEY_LAST_CRASH_AT, now) + .putInt(KEY_CRASH_COUNT, count) + .putString(KEY_NOTICE, summary) + .putBoolean(KEY_STARTUP_CRASH, isStartupCrash(error)) + .putBoolean(KEY_RETRY_GRANTED, false) + .commit(); + writeCrashLog(app, now, error); + if (count <= MAX_RESTARTS_IN_WINDOW) { + scheduleRestart(app); + } else { + preferences.edit().putString(KEY_NOTICE, + summary + ";一分钟内已连续异常 " + count + " 次,已停止自动重启以避免循环崩溃").commit(); + } + } + + static void recordRendererCrashAndRestart(Context context, boolean didCrash) { + RuntimeException error = new RuntimeException( + didCrash ? "WebView 渲染进程崩溃" : "WebView 渲染进程被系统回收"); + recordAndSchedule(context, error); + } + + static String consumeRecoveryNotice(Context context) { + SharedPreferences preferences = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE); + String notice = preferences.getString(KEY_NOTICE, ""); + if (notice == null || notice.length() == 0) { + return ""; + } + preferences.edit().remove(KEY_NOTICE).apply(); + return "应用已从异常退出中自动恢复:" + notice; + } + + static boolean shouldEnterStartupRecovery(Context context) { + SharedPreferences preferences = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE); + if (preferences.getBoolean(KEY_RETRY_GRANTED, false)) { + preferences.edit().putBoolean(KEY_RETRY_GRANTED, false).commit(); + return false; + } + boolean startupCrash = preferences.getBoolean(KEY_STARTUP_CRASH, false); + long lastCrashAt = preferences.getLong(KEY_LAST_CRASH_AT, 0L); + int count = preferences.getInt(KEY_CRASH_COUNT, 0); + boolean repeatedRecentCrash = count >= 2 + && System.currentTimeMillis() - lastCrashAt <= CRASH_WINDOW_MS; + return startupCrash || repeatedRecentCrash; + } + + static boolean canAutoRetryStartup(Context context) { + return context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) + .getInt(KEY_CRASH_COUNT, 0) <= MAX_RESTARTS_IN_WINDOW; + } + + static String peekRecoveryNotice(Context context) { + String notice = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) + .getString(KEY_NOTICE, ""); + return notice == null || notice.length() == 0 ? "未知异常" : notice; + } + + static void clearStartupRecoveryForRetry(Context context) { + context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) + .edit() + .putBoolean(KEY_STARTUP_CRASH, false) + .putBoolean(KEY_RETRY_GRANTED, true) + .commit(); + } + + static void markLaunchSuccessful(Context context) { + context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) + .edit() + .putLong(KEY_LAST_CRASH_AT, 0L) + .putInt(KEY_CRASH_COUNT, 0) + .putBoolean(KEY_STARTUP_CRASH, false) + .putBoolean(KEY_RETRY_GRANTED, false) + .apply(); + } + + private static void scheduleRestart(Context context) { + Intent intent = new Intent(context, MainActivity.class); + intent.setAction("com.xingyuan.zhiling.action.CRASH_RECOVERY"); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_CLEAR_TOP + | Intent.FLAG_ACTIVITY_SINGLE_TOP); + int flags = PendingIntent.FLAG_CANCEL_CURRENT; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + flags |= PendingIntent.FLAG_IMMUTABLE; + } + PendingIntent pendingIntent = PendingIntent.getActivity(context, 7311, intent, flags); + AlarmManager alarm = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + if (alarm == null) { + return; + } + long triggerAt = System.currentTimeMillis() + RESTART_DELAY_MS; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + alarm.setExact(AlarmManager.RTC_WAKEUP, triggerAt, pendingIntent); + } else { + alarm.set(AlarmManager.RTC_WAKEUP, triggerAt, pendingIntent); + } + } + + private static String summarize(Throwable error) { + if (error == null) { + return "未知异常"; + } + String type = error.getClass().getSimpleName(); + String message = error.getMessage(); + if (message == null || message.trim().length() == 0) { + return type; + } + String normalized = message.replace('\n', ' ').replace('\r', ' ').trim(); + if (normalized.length() > 180) { + normalized = normalized.substring(0, 180); + } + return type + " - " + normalized; + } + + private static boolean isStartupCrash(Throwable error) { + Throwable current = error; + while (current != null) { + StackTraceElement[] stack = current.getStackTrace(); + if (stack != null) { + for (StackTraceElement element : stack) { + if (MainActivity.class.getName().equals(element.getClassName()) + && "onCreate".equals(element.getMethodName())) { + return true; + } + } + } + current = current.getCause(); + } + return false; + } + + private static void writeCrashLog(Context context, long timestamp, Throwable error) { + File directory = new File(context.getFilesDir(), "crash_logs"); + if (!directory.exists() && !directory.mkdirs()) { + return; + } + File target = new File(directory, "last-crash.log"); + FileWriter writer = null; + try { + writer = new FileWriter(target, false); + writer.write(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.CHINA) + .format(new Date(timestamp))); + writer.write('\n'); + StringWriter stack = new StringWriter(); + PrintWriter printer = new PrintWriter(stack); + if (error != null) { + error.printStackTrace(printer); + } + printer.flush(); + writer.write(stack.toString()); + writer.flush(); + } catch (Throwable ignored) { + } finally { + if (writer != null) { + try { + writer.close(); + } catch (Exception ignored) { + } + } + } + } +} diff --git a/app/src/main/java/com/zhitou/terminal/FaceFeatureStore.java b/app/src/main/java/com/zhitou/terminal/FaceFeatureStore.java new file mode 100644 index 0000000..9007a7f --- /dev/null +++ b/app/src/main/java/com/zhitou/terminal/FaceFeatureStore.java @@ -0,0 +1,256 @@ +package com.xingyuan.zhiling; + +import android.content.Context; +import android.util.AtomicFile; +import android.util.Base64; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; + +import org.json.JSONArray; +import org.json.JSONObject; + +final class FaceFeatureStore { + static final class Match { + final String faceId; + final String employeeNo; + final String name; + final float similarity; + + Match(String faceId, String employeeNo, String name, float similarity) { + this.faceId = faceId; + this.employeeNo = employeeNo; + this.name = name; + this.similarity = similarity; + } + } + + private static final class Record { + final String faceId; + final String employeeNo; + final String name; + final float[] feature; + + Record(String faceId, String employeeNo, String name, float[] feature) { + this.faceId = faceId; + this.employeeNo = employeeNo; + this.name = name; + this.feature = feature; + } + } + + private static final Charset UTF8 = Charset.forName("UTF-8"); + private final AtomicFile atomicStore; + private final List records = new ArrayList(); + + FaceFeatureStore(Context context) { + atomicStore = new AtomicFile(new File(context.getFilesDir(), "xingyuan_face_features.json")); + load(); + } + + synchronized int size() { + return records.size(); + } + + synchronized JSONArray listPeople() { + JSONArray people = new JSONArray(); + for (Record record : records) { + try { + JSONObject person = new JSONObject(); + person.put("faceId", record.faceId); + person.put("employeeNo", record.employeeNo); + person.put("name", record.name); + person.put("registered", true); + people.put(person); + } catch (Exception ignored) { + } + } + return people; + } + + synchronized boolean remove(String faceId) throws Exception { + String normalizedId = safe(faceId); + List previous = new ArrayList(records); + boolean removed = false; + for (int i = records.size() - 1; i >= 0; i -= 1) { + if (normalizedId.equals(records.get(i).faceId)) { + records.remove(i); + removed = true; + } + } + if (removed) { + try { + save(); + } catch (Exception error) { + // 磁盘写入失败时恢复内存快照,避免运行中的名单与磁盘人脸库不一致。 + records.clear(); + records.addAll(previous); + throw error; + } + } + return removed; + } + + synchronized void put(String faceId, String employeeNo, String name, float[] feature) throws Exception { + if (faceId == null || faceId.trim().length() == 0 || feature == null || feature.length == 0) { + throw new IllegalArgumentException("人脸编号和特征不能为空"); + } + String normalizedId = faceId.trim(); + List previous = new ArrayList(records); + for (int i = records.size() - 1; i >= 0; i -= 1) { + if (normalizedId.equals(records.get(i).faceId)) { + records.remove(i); + } + } + records.add(new Record(normalizedId, safe(employeeNo), safe(name), feature.clone())); + try { + save(); + } catch (Exception error) { + records.clear(); + records.addAll(previous); + throw error; + } + } + + synchronized Match findBest(float[] probe, float threshold) { + if (probe == null || probe.length == 0) { + return null; + } + Record best = null; + float bestScore = -1f; + for (Record record : records) { + if (record.feature.length != probe.length) { + continue; + } + float score = cosine(record.feature, probe); + if (score > bestScore) { + bestScore = score; + best = record; + } + } + if (best == null || bestScore < threshold) { + return null; + } + return new Match(best.faceId, best.employeeNo, best.name, bestScore); + } + + private void load() { + synchronized (this) { + records.clear(); + if (!atomicStore.getBaseFile().exists()) { + return; + } + StringBuilder text = new StringBuilder(); + try { + // AtomicFile 会在上次写入中断时自动回退到 .bak,避免人员库变空。 + BufferedReader reader = new BufferedReader( + new InputStreamReader(atomicStore.openRead(), UTF8)); + try { + String line; + while ((line = reader.readLine()) != null) { + text.append(line); + } + } finally { + reader.close(); + } + JSONArray array = new JSONArray(text.toString()); + for (int i = 0; i < array.length(); i += 1) { + JSONObject item = array.optJSONObject(i); + if (item == null) { + continue; + } + float[] feature = decode(item.optString("feature", "")); + if (feature.length == 0) { + continue; + } + records.add(new Record( + item.optString("faceId", ""), + item.optString("employeeNo", ""), + item.optString("name", ""), + feature)); + } + } catch (Exception ignored) { + records.clear(); + } + } + } + + private void save() throws Exception { + JSONArray array = new JSONArray(); + for (Record record : records) { + JSONObject item = new JSONObject(); + item.put("faceId", record.faceId); + item.put("employeeNo", record.employeeNo); + item.put("name", record.name); + item.put("feature", encode(record.feature)); + array.put(item); + } + FileOutputStream output = null; + try { + output = atomicStore.startWrite(); + BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(output, UTF8)); + writer.write(array.toString()); + writer.flush(); + atomicStore.finishWrite(output); + } catch (Exception error) { + if (output != null) { + atomicStore.failWrite(output); + } + throw error; + } + } + + private static String encode(float[] feature) { + ByteBuffer buffer = ByteBuffer.allocate(feature.length * 4).order(ByteOrder.LITTLE_ENDIAN); + for (float value : feature) { + buffer.putFloat(value); + } + return Base64.encodeToString(buffer.array(), Base64.NO_WRAP); + } + + private static float[] decode(String value) { + try { + byte[] bytes = Base64.decode(value, Base64.DEFAULT); + if (bytes.length == 0 || bytes.length % 4 != 0) { + return new float[0]; + } + ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); + float[] feature = new float[bytes.length / 4]; + for (int i = 0; i < feature.length; i += 1) { + feature[i] = buffer.getFloat(); + } + return feature; + } catch (Exception ignored) { + return new float[0]; + } + } + + private static float cosine(float[] first, float[] second) { + double dot = 0; + double firstNorm = 0; + double secondNorm = 0; + for (int i = 0; i < first.length; i += 1) { + dot += first[i] * second[i]; + firstNorm += first[i] * first[i]; + secondNorm += second[i] * second[i]; + } + if (firstNorm <= 0 || secondNorm <= 0) { + return -1f; + } + return (float) (dot / Math.sqrt(firstNorm * secondNorm)); + } + + private static String safe(String value) { + return value == null ? "" : value.trim(); + } + +} diff --git a/app/src/main/java/com/zhitou/terminal/FaceRecognitionController.java b/app/src/main/java/com/zhitou/terminal/FaceRecognitionController.java new file mode 100644 index 0000000..3525394 --- /dev/null +++ b/app/src/main/java/com/zhitou/terminal/FaceRecognitionController.java @@ -0,0 +1,543 @@ +package com.xingyuan.zhiling; + +import android.content.Context; +import android.content.res.AssetManager; +import android.hardware.Camera; +import android.os.SystemClock; +import android.util.Log; +import android.view.SurfaceHolder; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; + +final class FaceRecognitionController implements Camera.PreviewCallback { + private static final String TAG = "XingyuanFace"; + interface Listener { + void onFaceStatus(String status, String message, int enrolledCount); + + void onFaceRecognized(FaceFeatureStore.Match match); + } + + private static final String MODEL_DIRECTORY = "face_models"; + private static final String DETECTOR_MODEL = "face_detector.csta"; + private static final String LANDMARKER_MODEL = "face_landmarker_pts5.csta"; + private static final String RECOGNIZER_MODEL = "face_recognizer.csta"; + private static final float DEFAULT_MATCH_THRESHOLD = 0.75f; + private static final long FRAME_INTERVAL_MS = 700; + private static final long RECOGNITION_COOLDOWN_MS = 5000; + private static final long ENROLLMENT_FEATURE_MAX_AGE_MS = 8000; + private static final long FEEDBACK_INTERVAL_MS = 1200; + private static final long CAMERA_RETRY_INTERVAL_MS = 5000; + + private final Context context; + private final Listener listener; + private final FaceFeatureStore featureStore; + private final ExecutorService worker = Executors.newSingleThreadExecutor(); + private final AtomicBoolean processing = new AtomicBoolean(false); + private final AtomicBoolean cameraTaskQueued = new AtomicBoolean(false); + private final AtomicBoolean destroyed = new AtomicBoolean(false); + private volatile Camera camera; + private volatile SurfaceHolder previewHolder; + private volatile boolean cameraStartRequestedWhileQueued; + private volatile boolean restartCameraWhenSurfaceReady; + private volatile boolean requestedActive; + private volatile long recognitionSession; + private volatile long lastCameraStartAttemptAt; + private volatile boolean engineReady; + private volatile int previewWidth; + private volatile int previewHeight; + private long lastFrameAt; + private long lastRecognizedAt; + private long lastSimilarityLogAt; + private long lastExtractionLogAt; + private long lastFeedbackAt; + private String lastFeedbackMessage = ""; + private volatile float[] latestFeature; + private volatile long latestFeatureAt; + private volatile float matchThreshold = DEFAULT_MATCH_THRESHOLD; + private boolean faceAvailableNotified; + + FaceRecognitionController(Context context, Listener listener) { + this.context = context.getApplicationContext(); + this.listener = listener; + this.featureStore = new FaceFeatureStore(context); + // 模型约 100MB,复制和 SeetaFace6 初始化必须离开 UI 线程,否则首次启动会 ANR。 + worker.execute(new Runnable() { + @Override + public void run() { + initializeEngine(); + } + }); + } + + int enrolledCount() { + return featureStore.size(); + } + + String peopleJson() { + return featureStore.listPeople().toString(); + } + + synchronized float setMatchThreshold(float value) { + if (Float.isNaN(value) || Float.isInfinite(value)) { + value = DEFAULT_MATCH_THRESHOLD; + } + matchThreshold = Math.max(0.50f, Math.min(0.95f, value)); + lastFeedbackAt = 0; + lastFeedbackMessage = ""; + Log.i(TAG, "match threshold updated=" + matchThreshold); + return matchThreshold; + } + + float getMatchThreshold() { + return matchThreshold; + } + + synchronized boolean removePerson(String faceId) throws Exception { + boolean removed = featureStore.remove(faceId); + if (removed) { + notifyStatus("ready", "人员已删除", featureStore.size()); + } + return removed; + } + + void setActive(boolean active) { + if (destroyed.get()) { + return; + } + if (requestedActive == active) { + if (active && camera == null) { + requestCameraStart(false); + } + return; + } + requestedActive = active; + recognitionSession++; + latestFeature = null; + latestFeatureAt = 0; + faceAvailableNotified = false; + if (active) { + lastCameraStartAttemptAt = 0; + lastFrameAt = 0; + lastRecognizedAt = 0; + lastSimilarityLogAt = 0; + lastExtractionLogAt = 0; + lastFeedbackAt = 0; + lastFeedbackMessage = ""; + Log.i(TAG, "resume recognition session=" + recognitionSession + + " enrolled=" + featureStore.size() + " cameraOpen=" + (camera != null)); + requestCameraStart(false); + } else { + // RK3399 的旧 Camera HAL 在页面切换时调用 stopPreview/release 可能阻塞数十秒。 + // 普通页面切换只暂停帧处理,保留摄像头预览;Activity 销毁时才真正释放。 + Log.i(TAG, "pause recognition session=" + recognitionSession); + } + } + + boolean isRequestedActive() { + return requestedActive; + } + + void retryAfterPermission() { + if (destroyed.get()) { + return; + } + if (requestedActive) { + lastCameraStartAttemptAt = 0; + requestCameraStart(false); + } + } + + void setPreviewSurface(SurfaceHolder holder) { + if (holder == null || destroyed.get()) { + return; + } + boolean changed = previewHolder != holder; + previewHolder = holder; + if (requestedActive && (camera == null || (changed && restartCameraWhenSurfaceReady))) { + requestCameraStart(changed && restartCameraWhenSurfaceReady); + } + } + + void clearPreviewSurface(SurfaceHolder holder) { + if (previewHolder != holder) { + return; + } + previewHolder = null; + if (camera != null) { + // 页面切换不会再销毁 Surface;若系统确实回收了 Surface,等新 Surface + // 创建后在后台线程重启摄像头,绝不在 UI 回调中调用 stopPreview。 + restartCameraWhenSurfaceReady = true; + } + } + + void destroy() { + if (!destroyed.compareAndSet(false, true)) { + return; + } + requestedActive = false; + recognitionSession++; + restartCameraWhenSurfaceReady = false; + previewHolder = null; + latestFeature = null; + latestFeatureAt = 0; + try { + worker.execute(new Runnable() { + @Override + public void run() { + stopCamera(); + if (engineReady) { + try { + SeetaFaceNative.release(); + } catch (Throwable ignored) { + } + engineReady = false; + } + } + }); + } catch (Throwable ignored) { + } + worker.shutdown(); + } + + synchronized void enrollCurrent(String faceId, String employeeNo, String name) throws Exception { + float[] snapshot = latestFeature; + long featureAge = SystemClock.elapsedRealtime() - latestFeatureAt; + if (snapshot == null || snapshot.length == 0 || featureAge > ENROLLMENT_FEATURE_MAX_AGE_MS) { + String reason = safeNativeError(); + if (reason.length() == 0 || "识别引擎初始化失败".equals(reason)) { + reason = "请正对摄像头,避免逆光并保持面部清晰"; + } + throw new IllegalStateException("最近 8 秒内未检测到可录入人脸:" + reason); + } + featureStore.put(faceId, employeeNo, name, snapshot); + notifyStatus("ready", "人脸采集成功", featureStore.size()); + } + + synchronized void prepareEnrollment() { + latestFeature = null; + latestFeatureAt = 0; + faceAvailableNotified = false; + notifyStatus("searching", "请正对摄像头,正在采集新的 SeetaFace6 人脸特征", featureStore.size()); + } + + private void initializeEngine() { + try { + File modelDirectory = new File(context.getFilesDir(), MODEL_DIRECTORY); + copyAsset(DETECTOR_MODEL, modelDirectory); + copyAsset(LANDMARKER_MODEL, modelDirectory); + copyAsset(RECOGNIZER_MODEL, modelDirectory); + engineReady = SeetaFaceNative.initialize( + new File(modelDirectory, DETECTOR_MODEL).getAbsolutePath(), + new File(modelDirectory, LANDMARKER_MODEL).getAbsolutePath(), + new File(modelDirectory, RECOGNIZER_MODEL).getAbsolutePath()); + notifyStatus(engineReady ? "ready" : "error", + engineReady ? "识别引擎已就绪" : safeNativeError(), featureStore.size()); + } catch (Throwable error) { + engineReady = false; + notifyStatus("error", "识别引擎加载失败:" + message(error), featureStore.size()); + } + } + + private void requestCameraStart(final boolean restart) { + if (destroyed.get()) { + return; + } + if (restart) { + restartCameraWhenSurfaceReady = true; + } + SurfaceHolder holder = previewHolder; + if (!requestedActive || holder == null || holder.getSurface() == null + || !holder.getSurface().isValid()) { + return; + } + long now = SystemClock.elapsedRealtime(); + if (!restart && camera == null && lastCameraStartAttemptAt > 0 + && now - lastCameraStartAttemptAt < CAMERA_RETRY_INTERVAL_MS) { + return; + } + if (!cameraTaskQueued.compareAndSet(false, true)) { + cameraStartRequestedWhileQueued = true; + return; + } + lastCameraStartAttemptAt = now; + cameraStartRequestedWhileQueued = false; + try { + worker.execute(new Runnable() { + @Override + public void run() { + try { + boolean shouldRestart = restartCameraWhenSurfaceReady; + restartCameraWhenSurfaceReady = false; + if (shouldRestart) { + stopCamera(); + } + startCamera(); + } finally { + boolean retryRequested = cameraStartRequestedWhileQueued; + cameraStartRequestedWhileQueued = false; + cameraTaskQueued.set(false); + if (requestedActive && previewHolder != null + && (retryRequested || restartCameraWhenSurfaceReady)) { + requestCameraStart(false); + } + } + } + }); + } catch (Throwable ignored) { + cameraTaskQueued.set(false); + lastCameraStartAttemptAt = 0; + } + } + + private synchronized void startCamera() { + if (destroyed.get() || !requestedActive || camera != null) { + return; + } + SurfaceHolder holder = previewHolder; + if (holder == null || holder.getSurface() == null || !holder.getSurface().isValid()) { + Log.i(TAG, "wait for persistent preview surface session=" + recognitionSession); + return; + } + Log.i(TAG, "open camera session=" + recognitionSession); + try { + int count = Camera.getNumberOfCameras(); + if (count <= 0) { + notifyStatus("camera_error", "未发现系统摄像头", featureStore.size()); + return; + } + camera = Camera.open(0); + Camera.Parameters parameters = camera.getParameters(); + Camera.Size selected = choosePreviewSize(parameters.getSupportedPreviewSizes()); + if (selected != null) { + parameters.setPreviewSize(selected.width, selected.height); + } + parameters.setPreviewFormat(android.graphics.ImageFormat.NV21); + camera.setParameters(parameters); + Camera.Size actual = camera.getParameters().getPreviewSize(); + previewWidth = actual.width; + previewHeight = actual.height; + camera.setPreviewDisplay(holder); + camera.setPreviewCallback(this); + camera.startPreview(); + notifyStatus(engineReady ? "recognizing" : "camera_ready", + engineReady + ? "摄像头已连接 " + previewWidth + "×" + previewHeight + ",识别中" + : "摄像头已连接,但识别引擎未就绪", + featureStore.size()); + } catch (SecurityException error) { + stopCamera(); + notifyStatus("permission", "等待摄像头权限", featureStore.size()); + } catch (Throwable error) { + stopCamera(); + notifyStatus("camera_error", "摄像头启动失败:" + message(error), featureStore.size()); + } + } + + private synchronized void stopCamera() { + recognitionSession++; + Log.i(TAG, "stop recognition session=" + recognitionSession); + Camera current = camera; + camera = null; + if (current != null) { + try { + current.setPreviewCallback(null); + current.stopPreview(); + } catch (Throwable ignored) { + } + try { + current.release(); + } catch (Throwable ignored) { + } + } + latestFeature = null; + latestFeatureAt = 0; + faceAvailableNotified = false; + } + + @Override + public void onPreviewFrame(final byte[] data, Camera source) { + if (destroyed.get() || !requestedActive || !engineReady + || data == null || previewWidth <= 0 || previewHeight <= 0) { + return; + } + long now = SystemClock.elapsedRealtime(); + if (now - lastFrameAt < FRAME_INTERVAL_MS || !processing.compareAndSet(false, true)) { + return; + } + lastFrameAt = now; + final byte[] frame = data.clone(); + final int width = previewWidth; + final int height = previewHeight; + final long session = recognitionSession; + try { + worker.execute(new Runnable() { + @Override + public void run() { + try { + long extractionStartedAt = SystemClock.elapsedRealtime(); + boolean shouldLogExtraction = extractionStartedAt - lastExtractionLogAt >= 3000; + if (shouldLogExtraction) { + lastExtractionLogAt = extractionStartedAt; + Log.i(TAG, "extract begin session=" + session + " size=" + width + "x" + height + + " enrolled=" + featureStore.size()); + } + float[] feature = SeetaFaceNative.extractFeature(frame, width, height, 0, false); + if (shouldLogExtraction) { + Log.i(TAG, "extract end session=" + session + + " elapsedMs=" + (SystemClock.elapsedRealtime() - extractionStartedAt) + + " feature=" + (feature == null ? 0 : feature.length) + + " nativeError=" + safeNativeError()); + } + if (!isRecognitionSessionCurrent(session)) { + Log.i(TAG, "discard stale frame session=" + session + + " current=" + recognitionSession); + return; + } + if (feature == null || feature.length == 0) { + notifyFaceFeedback("face_error", safeNativeError()); + if (latestFeatureAt > 0 + && SystemClock.elapsedRealtime() - latestFeatureAt > ENROLLMENT_FEATURE_MAX_AGE_MS) { + latestFeature = null; + latestFeatureAt = 0; + if (faceAvailableNotified) { + faceAvailableNotified = false; + notifyStatus("searching", "正在等待清晰正脸", featureStore.size()); + } + } + return; + } + latestFeature = feature; + latestFeatureAt = SystemClock.elapsedRealtime(); + if (!faceAvailableNotified) { + faceAvailableNotified = true; + notifyFaceFeedback("face_ready", "已检测到清晰正脸"); + } + long recognizedNow = SystemClock.elapsedRealtime(); + FaceFeatureStore.Match best = featureStore.findBest(feature, -1f); + float currentThreshold = matchThreshold; + if (best != null && recognizedNow - lastSimilarityLogAt >= 3000) { + lastSimilarityLogAt = recognizedNow; + Log.i(TAG, "best face=" + best.faceId + " similarity=" + best.similarity + + " threshold=" + currentThreshold); + } + FaceFeatureStore.Match match = best != null && best.similarity >= currentThreshold ? best : null; + if (match == null) { + if (best == null) { + notifyFaceFeedback("no_match", "本地没有可匹配的人脸信息"); + } else { + int similarityPercent = Math.round(best.similarity * 100f); + int thresholdPercent = Math.round(currentThreshold * 100f); + notifyFaceFeedback("no_match", "人脸匹配失败:相似度 " + + similarityPercent + "% ,需要达到 " + thresholdPercent + "%"); + } + } + if (match != null && recognizedNow - lastRecognizedAt >= RECOGNITION_COOLDOWN_MS) { + lastRecognizedAt = recognizedNow; + Log.i(TAG, "recognized face=" + match.faceId + " similarity=" + match.similarity); + if (isRecognitionSessionCurrent(session)) { + listener.onFaceRecognized(match); + } + } + } catch (Throwable error) { + notifyStatus("error", "人脸识别失败:" + message(error), featureStore.size()); + } finally { + processing.set(false); + } + } + }); + } catch (Throwable error) { + // Activity 销毁和 Camera 回调可能同时发生,线程池关闭后不能让拒绝异常逃出 Camera 线程。 + processing.set(false); + if (!destroyed.get()) { + Log.w(TAG, "discard preview frame because worker is unavailable", error); + } + } + } + + private boolean isRecognitionSessionCurrent(long session) { + return requestedActive && recognitionSession == session; + } + + private void notifyFaceFeedback(String status, String message) { + String normalized = message == null || message.length() == 0 ? "人脸识别失败" : message; + long now = SystemClock.elapsedRealtime(); + boolean sameMessage = normalized.equals(lastFeedbackMessage); + if (now - lastFeedbackAt < FEEDBACK_INTERVAL_MS + || (sameMessage && now - lastFeedbackAt < FEEDBACK_INTERVAL_MS * 3)) { + return; + } + lastFeedbackAt = now; + lastFeedbackMessage = normalized; + notifyStatus(status, normalized, featureStore.size()); + } + + private void copyAsset(String name, File directory) throws Exception { + if (!directory.exists() && !directory.mkdirs()) { + throw new IllegalStateException("无法创建模型目录"); + } + File target = new File(directory, name); + AssetManager assets = context.getAssets(); + InputStream input = assets.open(MODEL_DIRECTORY + "/" + name); + int assetSize = input.available(); + if (target.isFile() && assetSize > 0 && target.length() == assetSize) { + input.close(); + return; + } + FileOutputStream output = new FileOutputStream(target, false); + try { + byte[] buffer = new byte[16384]; + int count; + while ((count = input.read(buffer)) >= 0) { + output.write(buffer, 0, count); + } + } finally { + try { + input.close(); + } finally { + output.close(); + } + } + } + + private Camera.Size choosePreviewSize(List sizes) { + if (sizes == null || sizes.isEmpty()) { + return null; + } + Camera.Size best = sizes.get(0); + long bestDistance = Long.MAX_VALUE; + for (Camera.Size size : sizes) { + long distance = Math.abs(size.width - 640L) + Math.abs(size.height - 480L); + if (distance < bestDistance) { + best = size; + bestDistance = distance; + } + } + return best; + } + + private void notifyStatus(String status, String message, int count) { + if (!destroyed.get()) { + listener.onFaceStatus(status, message, count); + } + } + + private String safeNativeError() { + try { + String value = SeetaFaceNative.lastError(); + return value == null || value.length() == 0 ? "识别引擎初始化失败" : value; + } catch (Throwable ignored) { + return "识别引擎初始化失败"; + } + } + + private static String message(Throwable error) { + String value = error == null ? "未知错误" : error.getMessage(); + return value == null || value.length() == 0 ? error.getClass().getSimpleName() : value; + } +} diff --git a/app/src/main/java/com/zhitou/terminal/MainActivity.java b/app/src/main/java/com/zhitou/terminal/MainActivity.java new file mode 100644 index 0000000..9d8f074 --- /dev/null +++ b/app/src/main/java/com/zhitou/terminal/MainActivity.java @@ -0,0 +1,663 @@ +package com.xingyuan.zhiling; + +import android.Manifest; +import android.annotation.TargetApi; +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.ActivityNotFoundException; +import android.content.Intent; +import android.graphics.Color; +import android.graphics.PixelFormat; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Environment; +import android.provider.Settings; +import android.view.View; +import android.view.Gravity; +import android.view.Window; +import android.view.WindowManager; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.webkit.ValueCallback; +import android.webkit.RenderProcessGoneDetail; +import android.webkit.WebSettings; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import android.widget.FrameLayout; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; + +import java.io.File; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; + +import org.json.JSONObject; + +public class MainActivity extends Activity implements + ZhitouSerialPortManager.Listener, + ZhitouTcpReporter.Listener, + FaceRecognitionController.Listener { + static final int FILE_CHOOSER_REQUEST = 4101; + private static final int PERMISSION_REQUEST = 4102; + private FrameLayout rootLayout; + private WebView webView; + private SurfaceView facePreviewView; + private int facePreviewLeft = Integer.MIN_VALUE; + private int facePreviewTop = Integer.MIN_VALUE; + private int facePreviewWidth = Integer.MIN_VALUE; + private int facePreviewHeight = Integer.MIN_VALUE; + private volatile boolean requestedFacePreviewVisible; + private volatile int requestedFacePreviewLeft; + private volatile int requestedFacePreviewTop; + private volatile int requestedFacePreviewWidth; + private volatile int requestedFacePreviewHeight; + private final AtomicBoolean facePreviewLayoutQueued = new AtomicBoolean(false); + private final AtomicLong facePreviewLayoutVersion = new AtomicLong(0L); + private String pendingRecoveryNotice; + private boolean recoveryRetryStarted; + private final Runnable markStableLaunchTask = new Runnable() { + @Override + public void run() { + CrashRecovery.markLaunchSuccessful(MainActivity.this); + } + }; + private ValueCallback filePathCallback; + private ZhitouTcpReporter tcpReporter; + private ZhitouSerialPortManager serialPortManager; + private ZhitouBridge bridge; + private FaceRecognitionController faceRecognitionController; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (CrashRecovery.shouldEnterStartupRecovery(this)) { + showStartupRecoveryScreen(); + return; + } + pendingRecoveryNotice = CrashRecovery.consumeRecoveryNotice(this); + requestWindowFeature(Window.FEATURE_NO_TITLE); + getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + + rootLayout = new FrameLayout(this); + webView = new WebView(this); + tcpReporter = new ZhitouTcpReporter(this, this); + serialPortManager = new ZhitouSerialPortManager(this); + webView.setBackgroundColor(Color.rgb(247, 250, 248)); + rootLayout.addView(webView, new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT)); + facePreviewView = new SurfaceView(this); + facePreviewView.setZOrderOnTop(true); + facePreviewView.getHolder().setFormat(PixelFormat.OPAQUE); + // Surface 始终保留。RK3399 的旧 Camera HAL 在页面切换时重新绑定 + // Surface 会阻塞主线程并触发 ANR;隐藏时只把画面缩到 1×1。 + facePreviewView.setVisibility(View.VISIBLE); + rootLayout.addView(facePreviewView, new FrameLayout.LayoutParams(1, 1)); + setContentView(rootLayout); + + configureWebView(); + requestRuntimePermissions(); + prepareSmartStorage(); + webView.loadUrl("file:///android_asset/www/index.html"); + } + + @SuppressLint("SetJavaScriptEnabled") + private void configureWebView() { + WebSettings settings = webView.getSettings(); + settings.setJavaScriptEnabled(true); + settings.setDomStorageEnabled(true); + settings.setDatabaseEnabled(true); + settings.setAllowFileAccess(true); + settings.setAllowContentAccess(true); + settings.setLoadWithOverviewMode(true); + settings.setUseWideViewPort(true); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + // 页面只通过受控的原生桥通讯,不允许 file:// 页面任意读取文件或跨域访问。 + settings.setAllowFileAccessFromFileURLs(false); + settings.setAllowUniversalAccessFromFileURLs(false); + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + settings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW); + } + + webView.setWebViewClient(new RecoveryWebViewClient()); + webView.setWebChromeClient(new ZhitouWebChromeClient(this)); + faceRecognitionController = new FaceRecognitionController(this, this); + facePreviewView.getHolder().addCallback(new SurfaceHolder.Callback() { + @Override + public void surfaceCreated(SurfaceHolder holder) { + if (faceRecognitionController != null) { + faceRecognitionController.setPreviewSurface(holder); + } + } + + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + if (faceRecognitionController != null) { + faceRecognitionController.clearPreviewSurface(holder); + } + } + }); + bridge = new ZhitouBridge(this, tcpReporter, serialPortManager, faceRecognitionController); + webView.addJavascriptInterface(bridge, "XingyuanNative"); + webView.addJavascriptInterface(bridge, "ZhitouNative"); + } + + private void requestRuntimePermissions() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + return; + } + String[] permissions = new String[] { + Manifest.permission.CAMERA, + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE + }; + requestPermissions(permissions, PERMISSION_REQUEST); + } + + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (requestCode == PERMISSION_REQUEST) { + prepareSmartStorage(); + if (faceRecognitionController != null) { + faceRecognitionController.retryAfterPermission(); + } + } + } + + private void prepareSmartStorage() { + File smartDir = new File(Environment.getExternalStorageDirectory(), "smart"); + File imageDir = new File(smartDir, "Image"); + File logsDir = new File(smartDir, "logs"); + // 首次启动时创建业务固定目录,避免用户手动新建 smart/Image、smart/logs。 + ensureDirectory(imageDir); + ensureDirectory(logsDir); + } + + private void ensureDirectory(File dir) { + if (dir == null || dir.exists()) { + return; + } + try { + dir.mkdirs(); + } catch (Exception ignored) { + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode != FILE_CHOOSER_REQUEST || filePathCallback == null) { + return; + } + Uri[] results = null; + if (resultCode == RESULT_OK && data != null) { + if (data.getClipData() != null) { + int count = data.getClipData().getItemCount(); + results = new Uri[count]; + for (int i = 0; i < count; i += 1) { + results[i] = data.getClipData().getItemAt(i).getUri(); + } + } else if (data.getData() != null) { + results = new Uri[] { data.getData() }; + } + } + filePathCallback.onReceiveValue(results); + filePathCallback = null; + } + + void setFilePathCallback(ValueCallback callback) { + if (filePathCallback != null) { + filePathCallback.onReceiveValue(null); + } + filePathCallback = callback; + } + + void clearFilePathCallback() { + filePathCallback = null; + } + + void launchFileChooser(Intent intent) throws ActivityNotFoundException { + intent.addCategory(Intent.CATEGORY_OPENABLE); + startActivityForResult(intent, FILE_CHOOSER_REQUEST); + } + + void openSystemSettings() { + Intent intent = new Intent(Settings.ACTION_SETTINGS); + startActivity(intent); + } + + void openFileManager() { + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + try { + startActivity(intent); + } catch (ActivityNotFoundException error) { + openFileManagerFallback(); + } + } + + @Override + public void onImei(String imei) { + dispatchSerialImei(imei); + } + + @Override + public void onVersion(String version) { + dispatchSerialVersion(version); + } + + @Override + public void onIdentity(int type, String identity) { + dispatchSerialIdentity(type, identity); + } + + @Override + public void onThrowWeight(ZhitouSerialPortManager.ThrowWeightItem item) { + dispatchSerialThrowWeight(item); + } + + @Override + public void onDeviceStatus(ZhitouSerialPortManager.DeviceStatusItem item) { + dispatchSerialDeviceStatus(item); + } + + @Override + public void onReporterLog(String kind, String message) { + dispatchNativeLog(kind, message); + } + + private void dispatchSerialIdentity(int type, String identity) { + if (webView == null) { + return; + } + runOnUiThread(new ZhitouSerialIdentityTask(webView, type, identity)); + } + + private void dispatchSerialImei(String imei) { + if (webView == null) { + return; + } + runOnUiThread(new ZhitouSerialImeiTask(webView, imei)); + } + + private void dispatchSerialVersion(String version) { + if (webView == null) { + return; + } + runOnUiThread(new ZhitouSerialVersionTask(webView, version)); + } + + private void dispatchSerialThrowWeight(ZhitouSerialPortManager.ThrowWeightItem item) { + if (webView == null || item == null) { + return; + } + runOnUiThread(new ZhitouSerialThrowWeightTask(webView, item)); + } + + private void dispatchSerialDeviceStatus(ZhitouSerialPortManager.DeviceStatusItem item) { + if (webView == null || item == null) { + return; + } + runOnUiThread(new ZhitouSerialDeviceStatusTask(webView, item)); + } + + private void dispatchNativeLog(String kind, String message) { + if (webView == null) { + return; + } + runOnUiThread(new ZhitouNativeLogTask(webView, kind, message)); + } + + @Override + public void onFaceStatus(String status, String message, int enrolledCount) { + if (webView == null) { + return; + } + runOnUiThread(new ZhitouFaceStatusTask(webView, status, message, enrolledCount)); + } + + @Override + public void onFaceRecognized(FaceFeatureStore.Match match) { + if (webView == null || match == null) { + return; + } + runOnUiThread(new ZhitouFaceRecognizedTask(webView, match)); + } + + void setFaceRecognitionActive(boolean active) { + if (faceRecognitionController != null) { + faceRecognitionController.setActive(active); + } + } + + void setFacePreviewLayout(final boolean visible, final int left, final int top, final int width, final int height) { + requestedFacePreviewVisible = visible; + requestedFacePreviewLeft = left; + requestedFacePreviewTop = top; + requestedFacePreviewWidth = width; + requestedFacePreviewHeight = height; + facePreviewLayoutVersion.incrementAndGet(); + queueFacePreviewLayoutApply(); + } + + private void queueFacePreviewLayoutApply() { + if (!facePreviewLayoutQueued.compareAndSet(false, true)) { + return; + } + runOnUiThread(new Runnable() { + @Override + public void run() { + long appliedVersion = facePreviewLayoutVersion.get(); + applyFacePreviewLayout( + requestedFacePreviewVisible, + requestedFacePreviewLeft, + requestedFacePreviewTop, + requestedFacePreviewWidth, + requestedFacePreviewHeight); + facePreviewLayoutQueued.set(false); + if (facePreviewLayoutVersion.get() != appliedVersion) { + queueFacePreviewLayoutApply(); + } + } + }); + } + + private void applyFacePreviewLayout(boolean visible, int left, int top, int width, int height) { + if (facePreviewView == null || rootLayout == null) { + return; + } + int availableWidth = rootLayout.getWidth(); + int availableHeight = rootLayout.getHeight(); + if (availableWidth <= 0 || availableHeight <= 0) { + availableWidth = getResources().getDisplayMetrics().widthPixels; + availableHeight = getResources().getDisplayMetrics().heightPixels; + } + availableWidth = Math.max(1, availableWidth); + availableHeight = Math.max(1, availableHeight); + boolean showPreview = visible && width > 0 && height > 0; + int targetLeft = showPreview ? clamp(left, 0, availableWidth - 1) : 0; + int targetTop = showPreview ? clamp(top, 0, availableHeight - 1) : 0; + int targetWidth = showPreview ? clamp(width, 1, availableWidth - targetLeft) : 1; + int targetHeight = showPreview ? clamp(height, 1, availableHeight - targetTop) : 1; + boolean layoutChanged = facePreviewLeft != targetLeft + || facePreviewTop != targetTop + || facePreviewWidth != targetWidth + || facePreviewHeight != targetHeight; + if (layoutChanged) { + FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(targetWidth, targetHeight); + params.leftMargin = targetLeft; + params.topMargin = targetTop; + facePreviewView.setLayoutParams(params); + facePreviewLeft = targetLeft; + facePreviewTop = targetTop; + facePreviewWidth = targetWidth; + facePreviewHeight = targetHeight; + if (showPreview) { + facePreviewView.bringToFront(); + } + } + if (facePreviewView.getVisibility() != View.VISIBLE) { + facePreviewView.setVisibility(View.VISIBLE); + } + } + + private static int clamp(int value, int minimum, int maximum) { + return Math.max(minimum, Math.min(maximum, value)); + } + + int getEnrolledFaceCount() { + return faceRecognitionController == null ? 0 : faceRecognitionController.enrolledCount(); + } + + float setFaceMatchThreshold(float value) { + return faceRecognitionController == null ? value : faceRecognitionController.setMatchThreshold(value); + } + + float getFaceMatchThreshold() { + return faceRecognitionController == null ? 0.75f : faceRecognitionController.getMatchThreshold(); + } + + String getFacePeopleJson() { + return faceRecognitionController == null ? "[]" : faceRecognitionController.peopleJson(); + } + + boolean removeFacePerson(String faceId) throws Exception { + if (faceRecognitionController == null) { + throw new IllegalStateException("人脸识别控制器不可用"); + } + return faceRecognitionController.removePerson(faceId); + } + + void enrollCurrentFace(String faceId, String employeeNo, String name) throws Exception { + if (faceRecognitionController == null) { + throw new IllegalStateException("人脸识别控制器不可用"); + } + faceRecognitionController.enrollCurrent(faceId, employeeNo, name); + } + + void prepareFaceEnrollment() { + if (faceRecognitionController != null) { + faceRecognitionController.prepareEnrollment(); + } + } + + void dispatchConnectionCheck(JSONObject payload) { + if (webView == null) { + return; + } + runOnUiThread(new ZhitouConnectionCheckTask(webView, payload)); + } + + private void openFileManagerFallback() { + Intent fallback = new Intent(Intent.ACTION_GET_CONTENT); + fallback.setType("*/*"); + fallback.addCategory(Intent.CATEGORY_OPENABLE); + try { + startActivity(fallback); + } catch (ActivityNotFoundException error) { + Toast.makeText(this, "无法打开文件管理", Toast.LENGTH_SHORT).show(); + } + } + + @Override + public void onBackPressed() { + if (webView != null && webView.canGoBack()) { + webView.goBack(); + return; + } + super.onBackPressed(); + } + + @Override + protected void onResume() { + super.onResume(); + enterImmersiveMode(); + } + + @Override + public void onTrimMemory(int level) { + super.onTrimMemory(level); + if (level >= TRIM_MEMORY_MODERATE && webView != null) { + webView.freeMemory(); + } + } + + @Override + public void onLowMemory() { + super.onLowMemory(); + if (webView != null) { + webView.freeMemory(); + } + } + + @Override + protected void onDestroy() { + if (faceRecognitionController != null) { + faceRecognitionController.destroy(); + faceRecognitionController = null; + } + if (facePreviewView != null) { + facePreviewView.setVisibility(View.GONE); + } + if (bridge != null) { + bridge.destroy(); + bridge = null; + } + if (webView != null) { + webView.removeCallbacks(markStableLaunchTask); + webView.removeJavascriptInterface("XingyuanNative"); + webView.removeJavascriptInterface("ZhitouNative"); + webView.stopLoading(); + webView.loadUrl("about:blank"); + webView.setWebChromeClient(null); + webView.setWebViewClient(null); + if (rootLayout != null) { + rootLayout.removeView(webView); + } + webView.destroy(); + webView = null; + } + if (tcpReporter != null) { + tcpReporter.stop(); + } + if (serialPortManager != null) { + serialPortManager.destroy(); + } + super.onDestroy(); + } + + @Override + public void onWindowFocusChanged(boolean hasFocus) { + super.onWindowFocusChanged(hasFocus); + if (hasFocus) { + enterImmersiveMode(); + } + } + + private void enterImmersiveMode() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + getWindow().getDecorView().setSystemUiVisibility( + View.SYSTEM_UI_FLAG_FULLSCREEN + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_STABLE); + } + } + + private void showStartupRecoveryScreen() { + requestWindowFeature(Window.FEATURE_NO_TITLE); + getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, + WindowManager.LayoutParams.FLAG_FULLSCREEN); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + + LinearLayout panel = new LinearLayout(this); + panel.setOrientation(LinearLayout.VERTICAL); + panel.setGravity(Gravity.CENTER); + panel.setPadding(64, 64, 64, 64); + panel.setBackgroundColor(Color.rgb(247, 250, 248)); + + TextView title = new TextView(this); + title.setText("应用安全恢复"); + title.setTextSize(28f); + title.setTextColor(Color.rgb(29, 68, 51)); + title.setGravity(Gravity.CENTER); + panel.addView(title, new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT)); + + TextView detail = new TextView(this); + detail.setText("上次启动期间发生异常,已阻止循环崩溃。\n\n" + + CrashRecovery.peekRecoveryNotice(this)); + detail.setTextSize(18f); + detail.setTextColor(Color.DKGRAY); + detail.setGravity(Gravity.CENTER); + LinearLayout.LayoutParams detailParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT); + detailParams.setMargins(0, 40, 0, 40); + panel.addView(detail, detailParams); + + Button retry = new Button(this); + retry.setText("重新启动应用"); + retry.setTextSize(20f); + retry.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + retryAfterStartupCrash(); + } + }); + panel.addView(retry, new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT)); + setContentView(panel); + enterImmersiveMode(); + + if (CrashRecovery.canAutoRetryStartup(this)) { + panel.postDelayed(new Runnable() { + @Override + public void run() { + retryAfterStartupCrash(); + } + }, 4_000L); + } + } + + private void retryAfterStartupCrash() { + if (recoveryRetryStarted) { + return; + } + recoveryRetryStarted = true; + CrashRecovery.clearStartupRecoveryForRetry(this); + Intent retryIntent = new Intent(this, MainActivity.class); + retryIntent.setAction("com.xingyuan.zhiling.action.CRASH_RECOVERY"); + retryIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); + finish(); + startActivity(retryIntent); + } + + private final class RecoveryWebViewClient extends WebViewClient { + @Override + public void onPageFinished(WebView view, String url) { + super.onPageFinished(view, url); + view.removeCallbacks(markStableLaunchTask); + view.postDelayed(markStableLaunchTask, 30_000L); + if (pendingRecoveryNotice == null || pendingRecoveryNotice.length() == 0) { + return; + } + String notice = pendingRecoveryNotice; + pendingRecoveryNotice = ""; + new ZhitouNativeLogTask(view, "恢复", notice).run(); + Toast.makeText(MainActivity.this, notice, Toast.LENGTH_LONG).show(); + } + + @TargetApi(Build.VERSION_CODES.O) + @Override + public boolean onRenderProcessGone(WebView view, RenderProcessGoneDetail detail) { + boolean didCrash = detail != null && detail.didCrash(); + CrashRecovery.recordRendererCrashAndRestart(MainActivity.this, didCrash); + android.os.Process.killProcess(android.os.Process.myPid()); + return true; + } + } + + @SuppressWarnings("unused") + private void openApplicationSettings() { + Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + intent.setData(Uri.parse("package:" + getPackageName())); + startActivity(intent); + } +} diff --git a/app/src/main/java/com/zhitou/terminal/SeetaFaceNative.java b/app/src/main/java/com/zhitou/terminal/SeetaFaceNative.java new file mode 100644 index 0000000..2ab09b6 --- /dev/null +++ b/app/src/main/java/com/zhitou/terminal/SeetaFaceNative.java @@ -0,0 +1,23 @@ +package com.xingyuan.zhiling; + +final class SeetaFaceNative { + static { + System.loadLibrary("TenniS"); + System.loadLibrary("SeetaAuthorize"); + System.loadLibrary("SeetaFaceDetector600"); + System.loadLibrary("SeetaFaceLandmarker600"); + System.loadLibrary("SeetaFaceRecognizer600"); + System.loadLibrary("xingyuan_face"); + } + + private SeetaFaceNative() { + } + + static native boolean initialize(String detectorModel, String landmarkerModel, String recognizerModel); + + static native float[] extractFeature(byte[] nv21, int width, int height, int rotationDegrees, boolean mirror); + + static native String lastError(); + + static native void release(); +} diff --git a/app/src/main/java/com/zhitou/terminal/XingyuanApplication.java b/app/src/main/java/com/zhitou/terminal/XingyuanApplication.java new file mode 100644 index 0000000..fb7d9a0 --- /dev/null +++ b/app/src/main/java/com/zhitou/terminal/XingyuanApplication.java @@ -0,0 +1,47 @@ +package com.xingyuan.zhiling; + +import android.app.Application; +import android.os.Process; +import android.util.Log; + +import java.util.concurrent.atomic.AtomicBoolean; + +public final class XingyuanApplication extends Application { + private static final String TAG = "XingyuanCrash"; + private final AtomicBoolean handlingCrash = new AtomicBoolean(false); + + @Override + public void onCreate() { + super.onCreate(); + final Thread.UncaughtExceptionHandler previous = Thread.getDefaultUncaughtExceptionHandler(); + Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { + @Override + public void uncaughtException(Thread thread, Throwable error) { + if (!handlingCrash.compareAndSet(false, true)) { + terminate(previous, thread, error); + return; + } + try { + Log.e(TAG, "Uncaught exception in " + thread.getName(), error); + CrashRecovery.recordAndSchedule(XingyuanApplication.this, error); + } catch (Throwable recoveryError) { + Log.e(TAG, "Unable to record or schedule crash recovery", recoveryError); + } + Process.killProcess(Process.myPid()); + System.exit(10); + } + }); + } + + private static void terminate( + Thread.UncaughtExceptionHandler previous, + Thread thread, + Throwable error) { + if (previous != null) { + previous.uncaughtException(thread, error); + return; + } + Process.killProcess(Process.myPid()); + System.exit(10); + } +} diff --git a/app/src/main/java/com/zhitou/terminal/ZhitouBridge.java b/app/src/main/java/com/zhitou/terminal/ZhitouBridge.java new file mode 100644 index 0000000..4123f77 --- /dev/null +++ b/app/src/main/java/com/zhitou/terminal/ZhitouBridge.java @@ -0,0 +1,1061 @@ +package com.xingyuan.zhiling; + +import android.os.Environment; +import android.webkit.JavascriptInterface; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.json.JSONArray; +import org.json.JSONObject; + +final class ZhitouBridge { + private static final class ConnectionCheckTask implements Runnable { + private final ZhitouBridge bridge; + private final String json; + + ConnectionCheckTask(ZhitouBridge bridge, String json) { + this.bridge = bridge; + this.json = json; + } + + @Override + public void run() { + bridge.runConnectionCheck(json); + } + } + + private static final class SerialCheckTask implements Callable { + private final ZhitouSerialPortManager manager; + private final String path; + private final int baudRate; + + SerialCheckTask(ZhitouSerialPortManager manager, String path, int baudRate) { + this.manager = manager; + this.path = path; + this.baudRate = baudRate; + } + + @Override + public Boolean call() { + return manager.checkSerial(path, baudRate); + } + } + + private static final class TcpCheckTask implements Callable { + private final ZhitouBridge bridge; + private final String host; + private final int port; + private final int timeoutSeconds; + + TcpCheckTask(ZhitouBridge bridge, String host, int port, int timeoutSeconds) { + this.bridge = bridge; + this.host = host; + this.port = port; + this.timeoutSeconds = timeoutSeconds; + } + + @Override + public Boolean call() { + return host.length() > 0 && port > 0 && bridge.connectTcp(host, port, timeoutSeconds); + } + } + + private static final class PingCheckTask implements Callable { + private final ZhitouBridge bridge; + private final String host; + private final int timeoutSeconds; + + PingCheckTask(ZhitouBridge bridge, String host, int timeoutSeconds) { + this.bridge = bridge; + this.host = host; + this.timeoutSeconds = timeoutSeconds; + } + + @Override + public Boolean call() { + return bridge.ping(host.length() > 0 ? host : "www.baidu.com", timeoutSeconds); + } + } + + private final MainActivity activity; + private final ZhitouTcpReporter reporter; + private final ZhitouSerialPortManager serialPortManager; + private final FaceRecognitionController faceRecognitionController; + private final ExecutorService backgroundExecutor = Executors.newFixedThreadPool(4); + private final AtomicBoolean destroyed = new AtomicBoolean(false); + + ZhitouBridge( + MainActivity activity, + ZhitouTcpReporter reporter, + ZhitouSerialPortManager serialPortManager, + FaceRecognitionController faceRecognitionController) { + this.activity = activity; + this.reporter = reporter; + this.serialPortManager = serialPortManager; + this.faceRecognitionController = faceRecognitionController; + } + + void destroy() { + if (!destroyed.compareAndSet(false, true)) { + return; + } + backgroundExecutor.shutdownNow(); + } + + private boolean executeBackground(Runnable task) { + if (destroyed.get()) { + return false; + } + try { + backgroundExecutor.execute(task); + return true; + } catch (RejectedExecutionException ignored) { + // WebView 回调可能与 Activity 销毁同时到达,此时直接丢弃过期任务。 + return false; + } + } + + @JavascriptInterface + public void configure(String json) { + try { + JSONObject data = obj(json); + reporter.configure( + data.optString("host", "112.74.41.228"), + data.optInt("port", 9956), + data.optString("terminalId", "")); + } catch (Exception ignored) { + } + } + + @JavascriptInterface + public void startHeartbeat(String json) { + reporter.startHeartbeat(); + } + + @JavascriptInterface + public boolean pingHost(String json) { + try { + JSONObject data = obj(json); + String host = data.optString("host", "www.baidu.com").trim(); + int timeoutSeconds = data.optInt("timeoutSeconds", 3); + if (host.length() == 0) { + host = "www.baidu.com"; + } + return ping(host, timeoutSeconds); + } catch (Exception ignored) { + return false; + } + } + + @JavascriptInterface + public boolean checkTcpServer(String json) { + try { + JSONObject data = obj(json); + String host = data.optString("host", "112.74.41.228").trim(); + int port = data.optInt("port", 9956); + int timeoutSeconds = data.optInt("timeoutSeconds", 3); + if (host.length() == 0 || port <= 0) { + return false; + } + return connectTcp(host, port, timeoutSeconds); + } catch (Exception ignored) { + return false; + } + } + + @JavascriptInterface + public String getLowerImei(String json) { + try { + JSONObject data = obj(json); + return serialPortManager.requestImei( + data.optString("path", "/dev/ttyS4"), + data.optInt("baudRate", 9600)); + } catch (Exception ignored) { + return ""; + } + } + + @JavascriptInterface + public void requestLowerImei(String json) { + executeBackground(new ZhitouSerialImeiRequestTask(serialPortManager, json)); + } + + @JavascriptInterface + public boolean checkSerial(String json) { + try { + JSONObject data = obj(json); + return serialPortManager.checkSerial( + data.optString("path", "/dev/ttyS4"), + data.optInt("baudRate", 9600)); + } catch (Exception ignored) { + return false; + } + } + + /** 扫描常见 Android 串口设备节点,供设置页选择,不在扫描阶段占用串口。 */ + @JavascriptInterface + public String listSerialPorts(String json) { + JSONObject result = new JSONObject(); + JSONArray ports = new JSONArray(); + try { + File[] devices = new File("/dev").listFiles(); + if (devices == null) { + throw new IllegalStateException("无法读取 /dev 目录"); + } + List candidates = new ArrayList(); + for (File device : devices) { + String name = device == null ? "" : device.getName(); + if (isSerialDeviceName(name)) { + candidates.add(device); + } + } + Collections.sort(candidates, new Comparator() { + @Override + public int compare(File first, File second) { + return first.getAbsolutePath().compareToIgnoreCase(second.getAbsolutePath()); + } + }); + for (File device : candidates) { + JSONObject item = new JSONObject(); + item.put("path", device.getAbsolutePath()); + item.put("readable", device.canRead()); + item.put("writable", device.canWrite()); + ports.put(item); + } + result.put("ok", true); + result.put("ports", ports); + result.put("count", ports.length()); + } catch (Exception error) { + try { + result.put("ok", false); + result.put("ports", ports); + result.put("count", 0); + result.put("error", error.getMessage() == null ? "串口扫描失败" : error.getMessage()); + } catch (Exception ignored) { + } + } + return result.toString(); + } + + private boolean isSerialDeviceName(String name) { + return name != null && name.matches( + "^(ttyS|ttyUSB|ttyACM|ttyAMA|ttyFIQ|ttyXRUSB|ttyHS|ttyMT|ttymxc|ttysWK|ttyRK|ttyAP)[0-9]+$"); + } + + @JavascriptInterface + public String getLowerVersion(String json) { + try { + JSONObject data = obj(json); + return serialPortManager.requestVersion( + data.optString("path", "/dev/ttyS4"), + data.optInt("baudRate", 9600)); + } catch (Exception ignored) { + return ""; + } + } + + @JavascriptInterface + public void requestLowerVersion(String json) { + executeBackground(new ZhitouSerialVersionRequestTask(serialPortManager, json)); + } + + @JavascriptInterface + public void requestConnectionCheck(final String json) { + executeBackground(new ConnectionCheckTask(this, json)); + } + + private void runConnectionCheck(String json) { + JSONObject result = new JSONObject(); + try { + final JSONObject data = obj(json); + final String path = data.optString("path", "/dev/ttyS4"); + final int baudRate = data.optInt("baudRate", 9600); + final String host = data.optString("host", "112.74.41.228").trim(); + final int port = data.optInt("port", 9956); + final int timeoutSeconds = Math.max(1, Math.min(data.optInt("timeoutSeconds", 2), 5)); + final String pingHost = data.optString("pingHost", "www.baidu.com").trim(); + final String mode = data.optString("mode", "startup"); + Future serialFuture = null; + if (!"network".equals(mode)) { + serialFuture = backgroundExecutor.submit(new SerialCheckTask(serialPortManager, path, baudRate)); + } + Future tcpFuture = backgroundExecutor.submit(new TcpCheckTask(this, host, port, timeoutSeconds)); + Future pingFuture = backgroundExecutor.submit(new PingCheckTask(this, pingHost, timeoutSeconds)); + boolean serialOnline = serialFuture == null || futureBoolean(serialFuture, timeoutSeconds + 1); + boolean tcpConnected = futureBoolean(tcpFuture, timeoutSeconds + 1); + boolean pingOk = futureBoolean(pingFuture, timeoutSeconds + 1); + result.put("requestId", data.optInt("requestId", 0)); + result.put("mode", mode); + result.put("serialOnline", serialOnline); + result.put("tcpConnected", tcpConnected); + result.put("pingOk", pingOk); + result.put("networkOnline", tcpConnected || pingOk); + result.put("httpReachable", tcpConnected || pingOk); + } catch (Exception error) { + try { + result.put("mode", "startup"); + result.put("error", error.getMessage() == null ? "connection check failed" : error.getMessage()); + } catch (Exception ignored) { + } + } + if (!destroyed.get()) { + activity.dispatchConnectionCheck(result); + } + } + + private boolean futureBoolean(Future future, int timeoutSeconds) { + try { + return Boolean.TRUE.equals(future.get(timeoutSeconds, TimeUnit.SECONDS)); + } catch (Exception ignored) { + future.cancel(true); + return false; + } + } + + @JavascriptInterface + public boolean openThrowDoor(String json) { + try { + JSONObject data = obj(json); + return serialPortManager.openThrowDoor( + data.optString("path", "/dev/ttyS4"), + data.optInt("baudRate", 9600), + data.optInt("port", 0)); + } catch (Exception ignored) { + return false; + } + } + + @JavascriptInterface + public boolean closeThrowDoor(String json) { + try { + JSONObject data = obj(json); + return serialPortManager.closeThrowDoor( + data.optString("path", "/dev/ttyS4"), + data.optInt("baudRate", 9600)); + } catch (Exception ignored) { + return false; + } + } + + @JavascriptInterface + public boolean openReceiveDoor(String json) { + try { + JSONObject data = obj(json); + return serialPortManager.openReceiveDoor( + data.optString("path", "/dev/ttyS4"), + data.optInt("baudRate", 9600), + data.optInt("port", 1)); + } catch (Exception ignored) { + return false; + } + } + + @JavascriptInterface + public boolean queryWeight(String json) { + try { + JSONObject data = obj(json); + return serialPortManager.queryWeight( + data.optString("path", "/dev/ttyS4"), + data.optInt("baudRate", 9600), + data.optInt("port", 0)); + } catch (Exception ignored) { + return false; + } + } + + @JavascriptInterface + public boolean queryFill(String json) { + try { + JSONObject data = obj(json); + return serialPortManager.queryFill( + data.optString("path", "/dev/ttyS4"), + data.optInt("baudRate", 9600), + data.optInt("port", 0)); + } catch (Exception ignored) { + return false; + } + } + + @JavascriptInterface + public boolean queryTemperature(String json) { + try { + JSONObject data = obj(json); + return serialPortManager.queryTemperature( + data.optString("path", "/dev/ttyS4"), + data.optInt("baudRate", 9600), + data.optInt("port", 0)); + } catch (Exception ignored) { + return false; + } + } + + @JavascriptInterface + public void openSystemSettings(String json) { + activity.runOnUiThread(new ZhitouOpenSystemSettingsTask(activity)); + } + + @JavascriptInterface + public void openFileManager(String json) { + activity.runOnUiThread(new ZhitouOpenFileManagerTask(activity)); + } + + @JavascriptInterface + public void reportMemberVerify(String json) { + try { + JSONObject data = obj(json); + reporter.reportMemberVerify(data.optInt("idType", 2), data.optString("idContent", "")); + } catch (Exception ignored) { + } + } + + @JavascriptInterface + public String verifyMember(String json) { + try { + JSONObject data = obj(json); + return reporter.requestMemberVerify( + data.optInt("idType", 2), + data.optString("idContent", "")).toJson(); + } catch (Exception ignored) { + return ZhitouMemberInfo.failed("会员鉴权异常").toJson(); + } + } + + @JavascriptInterface + public String fetchWorkContext(String json) { + JSONArray machines = new JSONArray(); + try { + machines.put(new JSONObject() + .put("machineNo", "WAL-A01") + .put("persons", new JSONArray().put("张伟").put("李娜")) + .put("batches", new JSONArray().put("20260723-A01").put("20260723-A02"))); + machines.put(new JSONObject() + .put("machineNo", "WAL-A02") + .put("persons", new JSONArray().put("王强").put("赵敏")) + .put("batches", new JSONArray().put("20260723-B01").put("20260723-B02"))); + machines.put(new JSONObject() + .put("machineNo", "WAL-B01") + .put("persons", new JSONArray().put("陈晨").put("刘洋")) + .put("batches", new JSONArray().put("20260723-C01").put("20260723-C02"))); + machines.put(new JSONObject() + .put("machineNo", "WAL-B02") + .put("persons", new JSONArray().put("周杰").put("孙悦")) + .put("batches", new JSONArray().put("20260723-D01").put("20260723-D02"))); + } catch (Exception ignored) { + } + return machines.toString(); + } + + /** 人脸平台接口尚未接入;明确返回失败,避免把测试人员写入真实人脸库。 */ + @JavascriptInterface + public String syncFaceData(String json) { + JSONObject result = new JSONObject(); + try { + result.put("ok", false); + result.put("count", activity.getEnrolledFaceCount()); + result.put("error", "人员同步接口暂未接入"); + } catch (Exception ignored) { + } + return result.toString(); + } + + @JavascriptInterface + public String verifyFace(String json) { + try { + String faceId = obj(json).optString("faceId", "").trim(); + // 识别结果只允许在真实 1:N 特征库中核验,不能使用独立的测试名单缓存。 + JSONArray faces = new JSONArray(activity.getFacePeopleJson()); + for (int i = 0; i < faces.length(); i++) { + JSONObject face = faces.optJSONObject(i); + if (face != null && faceId.equals(face.optString("faceId"))) { + JSONObject result = new JSONObject(); + result.put("ok", true); + result.put("employeeNo", face.optString("employeeNo")); + result.put("memberCode", face.optString("employeeNo")); + result.put("memberId", face.optString("employeeNo")); + result.put("name", face.optString("name")); + result.put("balance", 0); + return result.toString(); + } + } + return ZhitouMemberInfo.failed("人脸识别失败").toJson(); + } catch (Exception ignored) { + return ZhitouMemberInfo.failed("人脸识别失败").toJson(); + } + } + + @JavascriptInterface + public void setFaceRecognitionActive(String json) { + try { + activity.setFaceRecognitionActive(obj(json).optBoolean("active", false)); + } catch (Exception ignored) { + } + } + + @JavascriptInterface + public void setFacePreviewLayout(String json) { + try { + JSONObject data = obj(json); + activity.setFacePreviewLayout( + data.optBoolean("visible", false), + data.optInt("left", 0), + data.optInt("top", 0), + data.optInt("width", 0), + data.optInt("height", 0)); + } catch (Exception ignored) { + } + } + + @JavascriptInterface + public String getFaceRecognitionStatus(String json) { + JSONObject result = new JSONObject(); + try { + result.put("ok", faceRecognitionController != null); + result.put("enrolledCount", activity.getEnrolledFaceCount()); + result.put("matchThreshold", activity.getFaceMatchThreshold()); + } catch (Exception ignored) { + } + return result.toString(); + } + + @JavascriptInterface + public String setFaceMatchThreshold(String json) { + JSONObject result = new JSONObject(); + try { + float requested = (float) obj(json).optDouble("threshold", 0.75); + float applied = activity.setFaceMatchThreshold(requested); + result.put("ok", true); + result.put("threshold", applied); + } catch (Exception error) { + try { + result.put("ok", false); + result.put("error", "人脸匹配阈值设置失败"); + } catch (Exception ignored) { + } + } + return result.toString(); + } + + @JavascriptInterface + public String listFacePeople(String json) { + JSONObject result = new JSONObject(); + try { + JSONArray people = new JSONArray(activity.getFacePeopleJson()); + result.put("ok", true); + result.put("people", people); + result.put("count", people.length()); + } catch (Exception error) { + try { + result.put("ok", false); + result.put("people", new JSONArray()); + result.put("error", "读取人员列表失败"); + } catch (Exception ignored) { + } + } + return result.toString(); + } + + @JavascriptInterface + public String deleteFacePerson(String json) { + JSONObject result = new JSONObject(); + try { + String faceId = obj(json).optString("faceId", "").trim(); + if (faceId.length() == 0) { + throw new IllegalArgumentException("人员编号不能为空"); + } + boolean removed = activity.removeFacePerson(faceId); + result.put("ok", removed); + result.put("count", activity.getEnrolledFaceCount()); + if (!removed) { + result.put("error", "未找到该人员"); + } + } catch (Exception error) { + try { + result.put("ok", false); + result.put("error", error.getMessage() == null ? "删除人员失败" : error.getMessage()); + } catch (Exception ignored) { + } + } + return result.toString(); + } + + @JavascriptInterface + public String enrollCurrentFace(String json) { + JSONObject result = new JSONObject(); + try { + JSONObject request = obj(json); + String employeeNo = request.optString("employeeNo", "").trim(); + String name = request.optString("name", "").trim(); + String faceId = request.optString("faceId", "").trim(); + if (employeeNo.length() == 0 || name.length() == 0) { + throw new IllegalArgumentException("请输入工号和姓名"); + } + if (faceId.length() == 0) { + faceId = "FACE-" + employeeNo; + } + activity.enrollCurrentFace(faceId, employeeNo, name); + result.put("ok", true); + result.put("faceId", faceId); + result.put("employeeNo", employeeNo); + result.put("name", name); + result.put("count", activity.getEnrolledFaceCount()); + } catch (Exception error) { + try { + result.put("ok", false); + result.put("error", error.getMessage() == null ? "人脸采集失败" : error.getMessage()); + } catch (Exception ignored) { + } + } + return result.toString(); + } + + @JavascriptInterface + public boolean prepareFaceEnrollment(String json) { + try { + activity.prepareFaceEnrollment(); + return true; + } catch (Exception ignored) { + return false; + } + } + + @JavascriptInterface + public boolean reportXingyuanDelivery(String json) { + try { + JSONObject data = obj(json); + reporter.reportDelivery( + data.optInt("idType", 0), + data.optString("idContent", data.optString("employeeNo", "")), + data.optInt("port", 0), + data.optInt("typeCode", 0), + data.optInt("range", 0), + data.optDouble("weight", 0), + data.optDouble("beforeWeight", 0), + data.optDouble("afterWeight", 0), + data.optLong("createdAt", System.currentTimeMillis())); + return true; + } catch (Exception ignored) { + return false; + } + } + + @JavascriptInterface + public void reportDelivery(String json) { + try { + JSONObject data = obj(json); + reporter.reportDelivery( + data.optInt("idType", 2), + data.optString("idContent", ""), + data.optInt("port", 0), + data.optInt("garbageType", 18), + data.optInt("range", 0), + data.optDouble("throwWeight", 0), + data.optDouble("beforeWeight", 0), + data.optDouble("afterWeight", 0), + data.optLong("createdAt", 0)); + } catch (Exception ignored) { + } + } + + @JavascriptInterface + public void reportDeviceStatus(String json) { + try { + JSONObject data = obj(json); + reporter.reportDeviceStatus(data); + } catch (Exception ignored) { + } + } + + @JavascriptInterface + public void reportAlarm(String json) { + try { + JSONObject data = obj(json); + JSONArray alarms = data.optJSONArray("alarms"); + if (alarms != null) { + reporter.reportAlarm(alarms); + } + } catch (Exception ignored) { + } + } + + @JavascriptInterface + public String getLocalBackupPath(String json) { + File dir = firstWritableCacheDir(); + if (dir == null) { + return ""; + } + return new File(dir, "xingyuan_zhiling_delivery_backup.csv").getAbsolutePath(); + } + + @JavascriptInterface + public String saveLocalBackup(String json) { + try { + JSONObject data = obj(json); + JSONObject record = data.optJSONObject("record"); + if (record == null) { + return ""; + } + File dir = firstWritableCacheDir(); + if (dir == null) { + return ""; + } + File jsonFile = new File(dir, "xingyuan_zhiling_delivery_backup.json"); + File jsonlFile = new File(dir, "xingyuan_zhiling_delivery_backup.jsonl"); + File csvFile = new File(dir, "xingyuan_zhiling_delivery_backup.csv"); + appendJsonRecord(jsonFile, record); + appendLine(jsonlFile, record.toString()); + boolean needHeader = !csvFile.exists() || csvFile.length() == 0; + BufferedWriter writer = new BufferedWriter(new FileWriter(csvFile, true)); + try { + if (needHeader) { + writer.write("设备编号,设备唯一识别码IMEI,设备名称,投放时间,员工姓名,员工工号,机台号,废料名,废料名ID,投放前重量,投放后重量,本次投放重量,投口,上传状态"); + writer.newLine(); + } + writer.write(csv(record.optString("deviceNo")) + + "," + csv(record.optString("deviceUniqueId")) + + "," + csv(record.optString("deviceName")) + + "," + csv(record.optString("deliveryTime", record.optString("time"))) + + "," + csv(record.optString("person")) + + "," + csv(record.optString("employeeNo")) + + "," + csv(record.optString("machineNo")) + + "," + csv(record.optString("garbageTypeName")) + + "," + csv(record.optString("typeId")) + + "," + csv(record.optString("beforeWeight")) + + "," + csv(record.optString("afterWeight")) + + "," + csv(record.optString("weight")) + + "," + csv(record.optString("port")) + + "," + csv(record.optString("uploadStatus"))); + writer.newLine(); + } finally { + writer.close(); + } + return csvFile.getAbsolutePath(); + } catch (Exception ignored) { + return ""; + } + } + + @JavascriptInterface + public String exportLogs(String json) { + try { + JSONObject data = obj(json); + JSONArray logs = data.optJSONArray("logs"); + File dir = firstWritableLogDir(); + if (dir == null) { + return ""; + } + String suffix = String.valueOf(data.optLong("exportedAt", System.currentTimeMillis())); + File jsonFile = new File(dir, "xingyuan_zhiling_logs_" + suffix + ".json"); + File textFile = new File(dir, "xingyuan_zhiling_logs_" + suffix + ".txt"); + BufferedWriter jsonWriter = new BufferedWriter(new FileWriter(jsonFile, false)); + try { + jsonWriter.write(data.toString()); + jsonWriter.newLine(); + } finally { + jsonWriter.close(); + } + BufferedWriter writer = new BufferedWriter(new FileWriter(textFile, false)); + try { + writer.write("导出时间: " + data.optString("exportedAtText")); + writer.newLine(); + writer.write("应用版本: " + data.optString("appVersion")); + writer.newLine(); + writer.write("设备编号: " + data.optString("deviceNo")); + writer.newLine(); + writer.write("终端号/IMEI: " + data.optString("terminalId")); + writer.newLine(); + writer.write("--------------------------------------------------"); + writer.newLine(); + if (logs != null) { + for (int i = 0; i < logs.length(); i += 1) { + JSONObject item = logs.optJSONObject(i); + if (item == null) { + continue; + } + writer.write(item.optLong("at") + " [" + item.optString("kind") + "] " + item.optString("message")); + String detail = item.optString("detail"); + if (detail != null && detail.length() > 0) { + writer.write(" - " + detail); + } + writer.newLine(); + } + } + } finally { + writer.close(); + } + return textFile.getAbsolutePath(); + } catch (Exception ignored) { + return ""; + } + } + + @JavascriptInterface + public String readLocalBackups(String json) { + for (File file : backupFileCandidates()) { + if (file == null || !file.exists() || !file.isFile()) { + continue; + } + JSONArray records = readBackupFile(file); + if (records != null) { + return records.toString(); + } + } + return new JSONArray().toString(); + } + + @JavascriptInterface + public String getCaptureImageDir(String json) { + File dir = firstWritableCaptureImageDir(); + if (dir == null) { + return ""; + } + return dir.getAbsolutePath(); + } + + @JavascriptInterface + public String prepareCaptureImage(String json) { + try { + JSONObject data = obj(json); + File dir = firstWritableCaptureImageDir(); + if (dir == null) { + return ""; + } + String fileName = captureFileName(data); + JSONObject result = new JSONObject(); + result.put("directory", dir.getAbsolutePath()); + result.put("fileName", fileName); + result.put("path", new File(dir, fileName).getAbsolutePath()); + return result.toString(); + } catch (Exception ignored) { + return ""; + } + } + + private JSONObject obj(String json) throws Exception { + if (json == null || json.length() == 0) { + return new JSONObject(); + } + return new JSONObject(json); + } + + private File firstWritableCacheDir() { + return firstWritableDir(new File[] { + new File(Environment.getExternalStorageDirectory(), "smart/cache"), + appExternalDirectory("smart/cache") + }); + } + + private File firstWritableLogDir() { + return firstWritableDir(new File[] { + new File(Environment.getExternalStorageDirectory(), "smart/logs"), + appExternalDirectory("smart/logs") + }); + } + + private File firstWritableCaptureImageDir() { + return firstWritableDir(new File[] { + new File(Environment.getExternalStorageDirectory(), "smart/Image"), + appExternalDirectory("smart/Image") + }); + } + + private File appExternalDirectory(String child) { + File base = activity.getExternalFilesDir(null); + return base == null ? null : new File(base, child); + } + + private File firstWritableDir(File[] candidates) { + for (File dir : candidates) { + if (dir == null) { + continue; + } + try { + if (!dir.exists() && !dir.mkdirs()) { + continue; + } + if (dir.isDirectory() && dir.canWrite()) { + return dir; + } + } catch (Exception ignored) { + } + } + return null; + } + + private String captureFileName(JSONObject data) { + String fileName = data.optString("fileName", "").trim(); + long createdAt = data.optLong("createdAt", System.currentTimeMillis()); + int port = data.optInt("port", 0); + int camera = data.optInt("camera", 0); + if (fileName.length() == 0) { + fileName = "IMG_" + createdAt + "_P" + port + "_C" + camera + ".jpg"; + } + fileName = fileName.replaceAll("[^A-Za-z0-9._-]", "_"); + if (fileName.length() == 0) { + fileName = "IMG_" + System.currentTimeMillis() + ".jpg"; + } + String lower = fileName.toLowerCase(Locale.ROOT); + if (!lower.endsWith(".jpg") && !lower.endsWith(".jpeg") && !lower.endsWith(".png")) { + fileName += ".jpg"; + } + return fileName; + } + + private File[] backupFileCandidates() { + return new File[] { + new File(Environment.getExternalStorageDirectory(), "smart/cache/xingyuan_zhiling_delivery_backup.json"), + appExternalDirectory("smart/cache/xingyuan_zhiling_delivery_backup.json"), + new File(Environment.getExternalStorageDirectory(), "smart/cache/xingyuan_zhiling_delivery_backup.jsonl"), + appExternalDirectory("smart/cache/xingyuan_zhiling_delivery_backup.jsonl"), + new File(Environment.getExternalStorageDirectory(), "smart/xingyuan_zhiling_delivery_backup.jsonl"), + appExternalDirectory("smart/xingyuan_zhiling_delivery_backup.jsonl") + }; + } + + private JSONArray readBackupFile(File file) { + JSONArray records = new JSONArray(); + BufferedReader reader = null; + try { + if (file.getName().toLowerCase(Locale.ROOT).endsWith(".json")) { + JSONArray saved = new JSONArray(readText(file)); + for (int i = 0; i < saved.length(); i += 1) { + JSONObject item = saved.optJSONObject(i); + if (item != null) { + records.put(item); + } + } + return records; + } + reader = new BufferedReader(new FileReader(file)); + String line; + while ((line = reader.readLine()) != null) { + line = line.trim(); + if (line.length() == 0) { + continue; + } + records.put(new JSONObject(line)); + } + return records; + } catch (Exception ignored) { + return null; + } finally { + if (reader != null) { + try { + reader.close(); + } catch (Exception ignored) { + } + } + } + } + + private void appendJsonRecord(File file, JSONObject record) throws Exception { + JSONArray records = new JSONArray(); + if (file.exists() && file.length() > 0) { + String text = readText(file).trim(); + if (text.length() > 0) { + try { + records = new JSONArray(text); + } catch (Exception ignored) { + records = new JSONArray(); + } + } + } + records.put(record); + BufferedWriter writer = new BufferedWriter(new FileWriter(file, false)); + try { + writer.write(records.toString()); + writer.newLine(); + } finally { + writer.close(); + } + } + + private String readText(File file) throws Exception { + StringBuilder builder = new StringBuilder(); + BufferedReader reader = new BufferedReader(new FileReader(file)); + try { + String line; + while ((line = reader.readLine()) != null) { + builder.append(line); + } + } finally { + reader.close(); + } + return builder.toString(); + } + + private void appendLine(File file, String line) throws Exception { + BufferedWriter writer = new BufferedWriter(new FileWriter(file, true)); + try { + writer.write(line); + writer.newLine(); + } finally { + writer.close(); + } + } + + private String csv(String value) { + String text = value == null ? "" : value; + return "\"" + text.replace("\"", "\"\"") + "\""; + } + + private boolean ping(String host, int timeoutSeconds) { + int timeout = Math.max(1, Math.min(timeoutSeconds, 10)); + String seconds = String.valueOf(timeout); + if (runPing(new String[] { "/system/bin/ping", "-c", "1", "-W", seconds, host })) { + return true; + } + return runPing(new String[] { "ping", "-c", "1", "-W", seconds, host }); + } + + private boolean connectTcp(String host, int port, int timeoutSeconds) { + Socket socket = null; + int timeoutMs = Math.max(1, Math.min(timeoutSeconds, 10)) * 1000; + try { + // 网络可用性以业务 TCP 可连为准,避免 ICMP ping 被系统或网络策略拦截后误报。 + socket = new Socket(); + socket.connect(new InetSocketAddress(host, port), timeoutMs); + return true; + } catch (Exception ignored) { + return false; + } finally { + if (socket != null) { + try { + socket.close(); + } catch (Exception ignored) { + } + } + } + } + + private boolean runPing(String[] command) { + Process process = null; + try { + process = Runtime.getRuntime().exec(command); + return process.waitFor() == 0; + } catch (Exception ignored) { + return false; + } finally { + if (process != null) { + process.destroy(); + } + } + } +} diff --git a/app/src/main/java/com/zhitou/terminal/ZhitouConnectionCheckTask.java b/app/src/main/java/com/zhitou/terminal/ZhitouConnectionCheckTask.java new file mode 100644 index 0000000..1a170e9 --- /dev/null +++ b/app/src/main/java/com/zhitou/terminal/ZhitouConnectionCheckTask.java @@ -0,0 +1,22 @@ +package com.xingyuan.zhiling; + +import android.webkit.WebView; + +import org.json.JSONObject; + +final class ZhitouConnectionCheckTask implements Runnable { + private final WebView webView; + private final JSONObject payload; + + ZhitouConnectionCheckTask(WebView webView, JSONObject payload) { + this.webView = webView; + this.payload = payload; + } + + @Override + public void run() { + String js = "window.ZhitouSerial&&window.ZhitouSerial.onConnectionCheck(" + + (payload == null ? "{}" : payload.toString()) + ");"; + webView.evaluateJavascript(js, null); + } +} diff --git a/app/src/main/java/com/zhitou/terminal/ZhitouDeviceStatusTask.java b/app/src/main/java/com/zhitou/terminal/ZhitouDeviceStatusTask.java new file mode 100644 index 0000000..e280807 --- /dev/null +++ b/app/src/main/java/com/zhitou/terminal/ZhitouDeviceStatusTask.java @@ -0,0 +1,14 @@ +package com.xingyuan.zhiling; + +final class ZhitouDeviceStatusTask implements Runnable { + private final ZhitouTcpReporter reporter; + + ZhitouDeviceStatusTask(ZhitouTcpReporter reporter) { + this.reporter = reporter; + } + + @Override + public void run() { + reporter.sendDeviceStatusNow(); + } +} diff --git a/app/src/main/java/com/zhitou/terminal/ZhitouFaceRecognizedTask.java b/app/src/main/java/com/zhitou/terminal/ZhitouFaceRecognizedTask.java new file mode 100644 index 0000000..0dc9a47 --- /dev/null +++ b/app/src/main/java/com/zhitou/terminal/ZhitouFaceRecognizedTask.java @@ -0,0 +1,33 @@ +package com.xingyuan.zhiling; + +import android.webkit.WebView; + +import org.json.JSONObject; + +final class ZhitouFaceRecognizedTask implements Runnable { + private final WebView webView; + private final FaceFeatureStore.Match match; + + ZhitouFaceRecognizedTask(WebView webView, FaceFeatureStore.Match match) { + this.webView = webView; + this.match = match; + } + + @Override + public void run() { + if (webView == null || match == null) { + return; + } + try { + JSONObject payload = new JSONObject(); + payload.put("faceId", match.faceId); + payload.put("employeeNo", match.employeeNo); + payload.put("name", match.name); + payload.put("similarity", match.similarity); + webView.evaluateJavascript( + "window.XingyuanSerial&&window.XingyuanSerial.onFaceRecognized(" + payload.toString() + ");", + null); + } catch (Exception ignored) { + } + } +} diff --git a/app/src/main/java/com/zhitou/terminal/ZhitouFaceStatusTask.java b/app/src/main/java/com/zhitou/terminal/ZhitouFaceStatusTask.java new file mode 100644 index 0000000..55bbd79 --- /dev/null +++ b/app/src/main/java/com/zhitou/terminal/ZhitouFaceStatusTask.java @@ -0,0 +1,36 @@ +package com.xingyuan.zhiling; + +import android.webkit.WebView; + +import org.json.JSONObject; + +final class ZhitouFaceStatusTask implements Runnable { + private final WebView webView; + private final String status; + private final String message; + private final int enrolledCount; + + ZhitouFaceStatusTask(WebView webView, String status, String message, int enrolledCount) { + this.webView = webView; + this.status = status; + this.message = message; + this.enrolledCount = enrolledCount; + } + + @Override + public void run() { + if (webView == null) { + return; + } + try { + JSONObject payload = new JSONObject(); + payload.put("status", status); + payload.put("message", message); + payload.put("enrolledCount", enrolledCount); + webView.evaluateJavascript( + "window.XingyuanSerial&&window.XingyuanSerial.onFaceStatus(" + payload.toString() + ");", + null); + } catch (Exception ignored) { + } + } +} diff --git a/app/src/main/java/com/zhitou/terminal/ZhitouHeartbeatTask.java b/app/src/main/java/com/zhitou/terminal/ZhitouHeartbeatTask.java new file mode 100644 index 0000000..be18678 --- /dev/null +++ b/app/src/main/java/com/zhitou/terminal/ZhitouHeartbeatTask.java @@ -0,0 +1,14 @@ +package com.xingyuan.zhiling; + +final class ZhitouHeartbeatTask implements Runnable { + private final ZhitouTcpReporter reporter; + + ZhitouHeartbeatTask(ZhitouTcpReporter reporter) { + this.reporter = reporter; + } + + @Override + public void run() { + reporter.sendHeartbeatNow(); + } +} diff --git a/app/src/main/java/com/zhitou/terminal/ZhitouMemberInfo.java b/app/src/main/java/com/zhitou/terminal/ZhitouMemberInfo.java new file mode 100644 index 0000000..d4fc644 --- /dev/null +++ b/app/src/main/java/com/zhitou/terminal/ZhitouMemberInfo.java @@ -0,0 +1,40 @@ +package com.xingyuan.zhiling; + +import org.json.JSONObject; + +final class ZhitouMemberInfo { + int verifyStatus; + String memberId = ""; + String memberCode = ""; + String name = ""; + String balance = ""; + int resiCount; + String error = ""; + + boolean isVerified() { + return verifyStatus == 1; + } + + String toJson() { + JSONObject data = new JSONObject(); + try { + data.put("ok", isVerified()); + data.put("verifyStatus", verifyStatus); + data.put("memberId", memberId == null ? "" : memberId); + data.put("memberCode", memberCode == null ? "" : memberCode); + data.put("name", name == null ? "" : name); + data.put("balance", balance == null ? "" : balance); + data.put("resiCount", resiCount); + data.put("error", error == null ? "" : error); + } catch (Exception ignored) { + } + return data.toString(); + } + + static ZhitouMemberInfo failed(String error) { + ZhitouMemberInfo info = new ZhitouMemberInfo(); + info.verifyStatus = 0; + info.error = error == null ? "" : error; + return info; + } +} diff --git a/app/src/main/java/com/zhitou/terminal/ZhitouNativeLogTask.java b/app/src/main/java/com/zhitou/terminal/ZhitouNativeLogTask.java new file mode 100644 index 0000000..7bd7a58 --- /dev/null +++ b/app/src/main/java/com/zhitou/terminal/ZhitouNativeLogTask.java @@ -0,0 +1,24 @@ +package com.xingyuan.zhiling; + +import android.webkit.WebView; + +import org.json.JSONObject; + +final class ZhitouNativeLogTask implements Runnable { + private final WebView webView; + private final String kind; + private final String message; + + ZhitouNativeLogTask(WebView webView, String kind, String message) { + this.webView = webView; + this.kind = kind == null ? "传输" : kind; + this.message = message == null ? "" : message; + } + + @Override + public void run() { + String js = "window.ZhitouSerial&&window.ZhitouSerial.onNativeLog(" + + JSONObject.quote(kind) + "," + JSONObject.quote(message) + ");"; + webView.evaluateJavascript(js, null); + } +} diff --git a/app/src/main/java/com/zhitou/terminal/ZhitouOpenFileManagerTask.java b/app/src/main/java/com/zhitou/terminal/ZhitouOpenFileManagerTask.java new file mode 100644 index 0000000..14c784a --- /dev/null +++ b/app/src/main/java/com/zhitou/terminal/ZhitouOpenFileManagerTask.java @@ -0,0 +1,14 @@ +package com.xingyuan.zhiling; + +final class ZhitouOpenFileManagerTask implements Runnable { + private final MainActivity activity; + + ZhitouOpenFileManagerTask(MainActivity activity) { + this.activity = activity; + } + + @Override + public void run() { + activity.openFileManager(); + } +} diff --git a/app/src/main/java/com/zhitou/terminal/ZhitouOpenSystemSettingsTask.java b/app/src/main/java/com/zhitou/terminal/ZhitouOpenSystemSettingsTask.java new file mode 100644 index 0000000..94d4374 --- /dev/null +++ b/app/src/main/java/com/zhitou/terminal/ZhitouOpenSystemSettingsTask.java @@ -0,0 +1,14 @@ +package com.xingyuan.zhiling; + +final class ZhitouOpenSystemSettingsTask implements Runnable { + private final MainActivity activity; + + ZhitouOpenSystemSettingsTask(MainActivity activity) { + this.activity = activity; + } + + @Override + public void run() { + activity.openSystemSettings(); + } +} diff --git a/app/src/main/java/com/zhitou/terminal/ZhitouSendFrameTask.java b/app/src/main/java/com/zhitou/terminal/ZhitouSendFrameTask.java new file mode 100644 index 0000000..bbcd1cc --- /dev/null +++ b/app/src/main/java/com/zhitou/terminal/ZhitouSendFrameTask.java @@ -0,0 +1,18 @@ +package com.xingyuan.zhiling; + +final class ZhitouSendFrameTask implements Runnable { + private final ZhitouTcpReporter reporter; + private final int orderCode; + private final byte[] body; + + ZhitouSendFrameTask(ZhitouTcpReporter reporter, int orderCode, byte[] body) { + this.reporter = reporter; + this.orderCode = orderCode; + this.body = body.clone(); + } + + @Override + public void run() { + reporter.sendFrameNow(orderCode, body); + } +} diff --git a/app/src/main/java/com/zhitou/terminal/ZhitouSerialDeviceStatusTask.java b/app/src/main/java/com/zhitou/terminal/ZhitouSerialDeviceStatusTask.java new file mode 100644 index 0000000..0d22588 --- /dev/null +++ b/app/src/main/java/com/zhitou/terminal/ZhitouSerialDeviceStatusTask.java @@ -0,0 +1,32 @@ +package com.xingyuan.zhiling; + +import android.webkit.WebView; + +import org.json.JSONObject; + +final class ZhitouSerialDeviceStatusTask implements Runnable { + private final WebView webView; + private final ZhitouSerialPortManager.DeviceStatusItem item; + + ZhitouSerialDeviceStatusTask(WebView webView, ZhitouSerialPortManager.DeviceStatusItem item) { + this.webView = webView; + this.item = item; + } + + @Override + public void run() { + JSONObject payload = new JSONObject(); + try { + payload.put("port", item.port); + if (!Double.isNaN(item.temperature)) { + payload.put("temperature", item.temperature); + } + if (item.range >= 0) { + payload.put("range", item.range); + } + } catch (Exception ignored) { + } + String js = "window.ZhitouSerial&&window.ZhitouSerial.onDeviceStatus(" + payload.toString() + ");"; + webView.evaluateJavascript(js, null); + } +} diff --git a/app/src/main/java/com/zhitou/terminal/ZhitouSerialIdentityTask.java b/app/src/main/java/com/zhitou/terminal/ZhitouSerialIdentityTask.java new file mode 100644 index 0000000..d2116a9 --- /dev/null +++ b/app/src/main/java/com/zhitou/terminal/ZhitouSerialIdentityTask.java @@ -0,0 +1,24 @@ +package com.xingyuan.zhiling; + +import android.webkit.WebView; + +import org.json.JSONObject; + +final class ZhitouSerialIdentityTask implements Runnable { + private final WebView webView; + private final int type; + private final String identity; + + ZhitouSerialIdentityTask(WebView webView, int type, String identity) { + this.webView = webView; + this.type = type; + this.identity = identity == null ? "" : identity; + } + + @Override + public void run() { + String js = "window.ZhitouSerial&&window.ZhitouSerial.onIdentity(" + + type + "," + JSONObject.quote(identity) + ");"; + webView.evaluateJavascript(js, null); + } +} diff --git a/app/src/main/java/com/zhitou/terminal/ZhitouSerialImeiRequestTask.java b/app/src/main/java/com/zhitou/terminal/ZhitouSerialImeiRequestTask.java new file mode 100644 index 0000000..d157555 --- /dev/null +++ b/app/src/main/java/com/zhitou/terminal/ZhitouSerialImeiRequestTask.java @@ -0,0 +1,24 @@ +package com.xingyuan.zhiling; + +import org.json.JSONObject; + +final class ZhitouSerialImeiRequestTask implements Runnable { + private final ZhitouSerialPortManager serialPortManager; + private final String json; + + ZhitouSerialImeiRequestTask(ZhitouSerialPortManager serialPortManager, String json) { + this.serialPortManager = serialPortManager; + this.json = json; + } + + @Override + public void run() { + try { + JSONObject data = json == null || json.length() == 0 ? new JSONObject() : new JSONObject(json); + serialPortManager.requestImei( + data.optString("path", "/dev/ttyS4"), + data.optInt("baudRate", 9600)); + } catch (Exception ignored) { + } + } +} diff --git a/app/src/main/java/com/zhitou/terminal/ZhitouSerialImeiTask.java b/app/src/main/java/com/zhitou/terminal/ZhitouSerialImeiTask.java new file mode 100644 index 0000000..16c6805 --- /dev/null +++ b/app/src/main/java/com/zhitou/terminal/ZhitouSerialImeiTask.java @@ -0,0 +1,22 @@ +package com.xingyuan.zhiling; + +import android.webkit.WebView; + +import org.json.JSONObject; + +final class ZhitouSerialImeiTask implements Runnable { + private final WebView webView; + private final String imei; + + ZhitouSerialImeiTask(WebView webView, String imei) { + this.webView = webView; + this.imei = imei == null ? "" : imei; + } + + @Override + public void run() { + String js = "window.ZhitouSerial&&window.ZhitouSerial.onImei(" + + JSONObject.quote(imei) + ");"; + webView.evaluateJavascript(js, null); + } +} diff --git a/app/src/main/java/com/zhitou/terminal/ZhitouSerialPortManager.java b/app/src/main/java/com/zhitou/terminal/ZhitouSerialPortManager.java new file mode 100644 index 0000000..e9ba98d --- /dev/null +++ b/app/src/main/java/com/zhitou/terminal/ZhitouSerialPortManager.java @@ -0,0 +1,743 @@ +package com.xingyuan.zhiling; + +import android.util.Log; + +import java.io.File; +import java.io.InputStream; +import java.io.OutputStream; +import java.math.BigDecimal; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.RejectedExecutionException; + +import android_serialport_api.SerialPort; + +final class ZhitouSerialPortManager { + private static final class SerialWriteTask implements Runnable { + private final ZhitouSerialPortManager manager; + private final byte[] frame; + private final long generation; + + SerialWriteTask(ZhitouSerialPortManager manager, byte[] frame, long generation) { + this.manager = manager; + this.frame = frame; + this.generation = generation; + } + + @Override + public void run() { + manager.writeFrame(frame, generation); + } + } + + interface Listener { + void onImei(String imei); + + void onVersion(String version); + + void onIdentity(int type, String identity); + + void onThrowWeight(ThrowWeightItem item); + + void onDeviceStatus(DeviceStatusItem item); + } + + static final class ThrowWeightItem { + final int port; + final double weight; + final double beforeWeight; + final double afterWeight; + final int range; + final boolean queryResponse; + + ThrowWeightItem(int port, double weight, double beforeWeight, double afterWeight, int range, + boolean queryResponse) { + this.port = port; + this.weight = weight; + this.beforeWeight = beforeWeight; + this.afterWeight = afterWeight; + this.range = range; + this.queryResponse = queryResponse; + } + } + + static final class DeviceStatusItem { + final int port; + final double temperature; + final int range; + + DeviceStatusItem(int port, double temperature, int range) { + this.port = port; + this.temperature = temperature; + this.range = range; + } + } + + private static final String TAG = "ZhitouSerial"; + private static final String DEFAULT_PATH = "/dev/ttyS4"; + private static final int DEFAULT_BAUD_RATE = 9600; + private static final long SERIAL_CHECK_WAIT_MS = 1800; + private static final long WRITE_GAP_MS = 40; + private static final Charset GBK = Charset.forName("GBK"); + private static final byte ORDER_MEMBER_VERIFY = 0x01; + private static final byte ORDER_OPEN_THROW_DOOR = 0x02; + private static final byte ORDER_CLOSE_THROW_DOOR = 0x03; + private static final byte ORDER_OPEN_RECEIVE_DOOR = 0x04; + private static final byte ORDER_QUERY_WEIGHT = 0x20; + private static final byte ORDER_QUERY_FILL = 0x21; + private static final byte ORDER_QUERY_TEMPERATURE = 0x22; + private static final byte ORDER_DEVICE_VERSION = 0x30; + private static final byte ORDER_DEVICE_IMEI = 0x31; + private static final byte ORDER_CLOSE_TIME = 0x60; + private static final byte ORDER_THROW_WEIGHT = 0x61; + private static final byte ORDER_DEVICE_STATUS = 0x62; + private static final byte ORDER_GPS = 0x67; + private static final byte ORDER_POWER_ON = (byte) 0x90; + + private final Listener listener; + private final Object ioLock = new Object(); + private final Object imeiLock = new Object(); + private final Object versionLock = new Object(); + private final byte[] receiveBuffer = new byte[4096]; + private final ExecutorService writeExecutor = Executors.newSingleThreadExecutor(); + + private SerialPort serialPort; + private InputStream inputStream; + private OutputStream outputStream; + private Thread readThread; + private String currentPath = DEFAULT_PATH; + private int currentBaudRate = DEFAULT_BAUD_RATE; + private int receiveLength; + private volatile boolean running; + private volatile String lastImei = ""; + private volatile String lastVersion = ""; + private volatile long lastValidFrameAt; + private volatile long connectionGeneration; + private volatile boolean destroyed; + + ZhitouSerialPortManager(Listener listener) { + this.listener = listener; + } + + boolean checkSerial(String path, int baudRate) { + long baseline; + if (!ensureOpen(path, baudRate)) { + return false; + } + if (hasRecentValidFrame()) { + return true; + } + baseline = lastValidFrameAt; + // 启动自检必须等到下位机真实回包,不能只用“串口能打开”判断在线。 + if (!sendFrame(ORDER_DEVICE_IMEI, new byte[0])) { + return false; + } + return waitForValidFrameAfter(baseline, SERIAL_CHECK_WAIT_MS); + } + + String requestImei(String path, int baudRate) { + if (!ensureOpen(path, baudRate)) { + return ""; + } + synchronized (imeiLock) { + lastImei = ""; + } + sendFrame(ORDER_DEVICE_IMEI, new byte[0]); + return waitForValue(imeiLock, true); + } + + String requestVersion(String path, int baudRate) { + if (!ensureOpen(path, baudRate)) { + return ""; + } + synchronized (versionLock) { + lastVersion = ""; + } + sendFrame(ORDER_DEVICE_VERSION, new byte[0]); + return waitForValue(versionLock, false); + } + + boolean openThrowDoor(String path, int baudRate, int port) { + if (!ensureOpen(path, baudRate)) { + return false; + } + return sendFrame(ORDER_OPEN_THROW_DOOR, new byte[] { 0x01, (byte) clamp(port, 0, 255) }); + } + + boolean closeThrowDoor(String path, int baudRate) { + if (!ensureOpen(path, baudRate)) { + return false; + } + return sendFrame(ORDER_CLOSE_THROW_DOOR, new byte[] { 0x01 }); + } + + boolean openReceiveDoor(String path, int baudRate, int doorIndex) { + if (!ensureOpen(path, baudRate)) { + return false; + } + return sendFrame(ORDER_OPEN_RECEIVE_DOOR, new byte[] { 0x01, (byte) clamp(doorIndex, 0, 255) }); + } + + boolean queryWeight(String path, int baudRate, int port) { + if (!ensureOpen(path, baudRate)) { + return false; + } + return sendFrame(ORDER_QUERY_WEIGHT, new byte[] { 0x01, (byte) clamp(port, 0, 255) }); + } + + boolean queryFill(String path, int baudRate, int port) { + if (!ensureOpen(path, baudRate)) { + return false; + } + return sendFrame(ORDER_QUERY_FILL, new byte[] { 0x01, (byte) clamp(port, 0, 255) }); + } + + boolean queryTemperature(String path, int baudRate, int port) { + if (!ensureOpen(path, baudRate)) { + return false; + } + return sendFrame(ORDER_QUERY_TEMPERATURE, new byte[] { 0x01, (byte) clamp(port, 0, 255) }); + } + + void close() { + synchronized (ioLock) { + connectionGeneration += 1; + running = false; + if (inputStream != null) { + try { + inputStream.close(); + } catch (Exception ignored) { + } + } + inputStream = null; + if (outputStream != null) { + try { + outputStream.close(); + } catch (Exception ignored) { + } + } + outputStream = null; + if (serialPort != null) { + try { + serialPort.close(); + } catch (Exception ignored) { + } + } + serialPort = null; + readThread = null; + receiveLength = 0; + lastValidFrameAt = 0; + } + } + + void destroy() { + destroyed = true; + close(); + writeExecutor.shutdownNow(); + } + + private boolean ensureOpen(String path, int baudRate) { + if (destroyed) { + return false; + } + String nextPath = normalizePath(path); + int nextBaudRate = baudRate > 0 ? baudRate : DEFAULT_BAUD_RATE; + synchronized (ioLock) { + if (serialPort != null && outputStream != null && inputStream != null + && nextPath.equals(currentPath) && nextBaudRate == currentBaudRate) { + return true; + } + close(); + currentPath = nextPath; + currentBaudRate = nextBaudRate; + try { + serialPort = new SerialPort(new File(currentPath), currentBaudRate, 0); + inputStream = serialPort.getInputStream(); + outputStream = serialPort.getOutputStream(); + running = true; + long readGeneration = connectionGeneration; + readThread = new Thread( + new ZhitouSerialReadLoop(this, readGeneration), + "zhitou-serial-read"); + readThread.setDaemon(true); + readThread.start(); + Log.i(TAG, "opened " + currentPath + " baud=" + currentBaudRate); + return true; + } catch (Throwable error) { + Log.e(TAG, "open failed path=" + currentPath + " baud=" + currentBaudRate, error); + close(); + return false; + } + } + } + + private boolean sendFrame(byte orderCode, byte[] body) { + final byte[] frame = buildFrame(orderCode, body == null ? new byte[0] : body); + final long generation; + synchronized (ioLock) { + if (destroyed || outputStream == null) { + return false; + } + generation = connectionGeneration; + } + try { + writeExecutor.execute(new SerialWriteTask(this, frame, generation)); + return true; + } catch (RejectedExecutionException error) { + Log.w(TAG, "send rejected because serial manager is shutting down"); + return false; + } + } + + private void writeFrame(byte[] frame, long generation) { + synchronized (ioLock) { + if (destroyed || generation != connectionGeneration || outputStream == null) { + return; + } + try { + outputStream.write(frame); + outputStream.flush(); + Log.i(TAG, "send " + toHex(frame)); + } catch (Exception error) { + Log.e(TAG, "send failed " + toHex(frame), error); + close(); + return; + } + } + try { + Thread.sleep(WRITE_GAP_MS); + } catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); + } + } + + private byte[] buildFrame(byte orderCode, byte[] body) { + byte[] result = new byte[body.length + 10]; + int index = 0; + result[index++] = (byte) 0xfe; + result[index++] = (byte) 0xfe; + result[index++] = 0x68; + result[index++] = 0x00; + result[index++] = 0x68; + result[index++] = orderCode; + result[index++] = (byte) ((body.length >> 8) & 0xff); + result[index++] = (byte) (body.length & 0xff); + System.arraycopy(body, 0, result, index, body.length); + index += body.length; + result[index++] = checksum(orderCode, body); + result[index] = 0x16; + return result; + } + + private byte checksum(byte orderCode, byte[] body) { + int sum = 0x68 + 0x00 + 0x68 + unsigned(orderCode); + sum += (body.length >> 8) & 0xff; + sum += body.length & 0xff; + for (byte b : body) { + sum += unsigned(b); + } + return (byte) sum; + } + + private String waitForValue(Object lock, boolean imei) { + long deadline = System.currentTimeMillis() + 2200; + synchronized (lock) { + while ((imei ? lastImei : lastVersion).length() == 0) { + long remaining = deadline - System.currentTimeMillis(); + if (remaining <= 0) { + break; + } + try { + lock.wait(remaining); + } catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); + break; + } + } + return imei ? lastImei : lastVersion; + } + } + + boolean isRunning(long generation) { + return running && generation == connectionGeneration; + } + + void readSerialOnce(long generation) { + if (!isRunning(generation)) { + return; + } + InputStream stream = inputStream; + if (stream == null) { + return; + } + try { + byte[] chunk = new byte[128]; + int size = stream.read(chunk); + if (size > 0 && isRunning(generation)) { + appendReceived(chunk, size); + } else if (size < 0) { + // EOF 表示本次连接已经失效,必须退出读循环,避免空转占满 CPU。 + closeIfCurrent(generation); + } + } catch (Exception error) { + if (isRunning(generation)) { + Log.e(TAG, "read failed", error); + closeIfCurrent(generation); + } + } + } + + private void closeIfCurrent(long generation) { + synchronized (ioLock) { + if (generation != connectionGeneration) { + return; + } + // 连接代次用于隔离旧读线程,避免快速重连后两个线程同时读取新串口。 + close(); + } + } + + private void appendReceived(byte[] chunk, int size) { + synchronized (receiveBuffer) { + if (size > receiveBuffer.length) { + size = receiveBuffer.length; + } + if (receiveLength + size > receiveBuffer.length) { + receiveLength = 0; + } + System.arraycopy(chunk, 0, receiveBuffer, receiveLength, size); + receiveLength += size; + Log.i(TAG, "recv " + toHex(chunk, size)); + parseFrames(); + } + } + + private void parseFrames() { + int offset = 0; + while (true) { + int header = findHeader(offset); + if (header < 0) { + retainPartialHeader(); + return; + } + if (header > 0) { + shiftLeft(header); + header = 0; + } + if (receiveLength < 8) { + return; + } + int length = (unsigned(receiveBuffer[4]) << 8) | unsigned(receiveBuffer[5]); + int total = 8 + length; + if (total > receiveBuffer.length) { + shiftLeft(1); + continue; + } + if (receiveLength < total) { + return; + } + if (receiveBuffer[total - 1] != 0x16) { + shiftLeft(1); + continue; + } + byte order = receiveBuffer[3]; + byte[] body = new byte[length]; + System.arraycopy(receiveBuffer, 6, body, 0, length); + byte actualChecksum = receiveBuffer[6 + length]; + byte expectedChecksum = checksum(order, body); + if (actualChecksum == expectedChecksum) { + lastValidFrameAt = System.currentTimeMillis(); + handleFrame(order, body); + } else { + Log.w(TAG, "checksum mismatch order=" + unsigned(order) + + " expected=" + unsigned(expectedChecksum) + + " actual=" + unsigned(actualChecksum)); + } + shiftLeft(total); + } + } + + private int findHeader(int offset) { + for (int i = offset; i + 2 < receiveLength; i += 1) { + if (receiveBuffer[i] == 0x68 && receiveBuffer[i + 1] == 0x00 && receiveBuffer[i + 2] == 0x68) { + return i; + } + } + return -1; + } + + private void retainPartialHeader() { + int keep = 0; + if (receiveLength >= 2 + && receiveBuffer[receiveLength - 2] == 0x68 + && receiveBuffer[receiveLength - 1] == 0x00) { + keep = 2; + } else if (receiveLength >= 1 && receiveBuffer[receiveLength - 1] == 0x68) { + keep = 1; + } + if (keep > 0) { + System.arraycopy(receiveBuffer, receiveLength - keep, receiveBuffer, 0, keep); + } + receiveLength = keep; + } + + private void shiftLeft(int count) { + if (count <= 0) { + return; + } + if (count >= receiveLength) { + receiveLength = 0; + return; + } + System.arraycopy(receiveBuffer, count, receiveBuffer, 0, receiveLength - count); + receiveLength -= count; + } + + private void handleFrame(byte order, byte[] body) { + int code = unsigned(order); + if (code == unsigned(ORDER_MEMBER_VERIFY)) { + handleIdentity(body); + sendFrame(ORDER_MEMBER_VERIFY, new byte[0]); + } else if (code == unsigned(ORDER_DEVICE_IMEI)) { + handleImei(body); + } else if (code == unsigned(ORDER_DEVICE_VERSION)) { + handleVersion(body); + } else if (code == unsigned(ORDER_OPEN_RECEIVE_DOOR)) { + Log.i(TAG, "receive door command result"); + } else if (code == unsigned(ORDER_QUERY_WEIGHT)) { + handleQueryWeight(body); + } else if (code == unsigned(ORDER_QUERY_FILL)) { + handleQueryFill(body); + } else if (code == unsigned(ORDER_QUERY_TEMPERATURE)) { + handleQueryTemperature(body); + } else if (code == unsigned(ORDER_THROW_WEIGHT)) { + handleThrowWeight(body); + sendFrame(ORDER_THROW_WEIGHT, new byte[0]); + } else if (code == unsigned(ORDER_DEVICE_STATUS)) { + sendFrame(ORDER_DEVICE_STATUS, new byte[0]); + handleDeviceStatus(body); + } else if (code == unsigned(ORDER_CLOSE_TIME)) { + sendFrame(ORDER_CLOSE_TIME, new byte[0]); + } else if (code == unsigned(ORDER_GPS)) { + sendFrame(ORDER_GPS, new byte[0]); + } else if (code == unsigned(ORDER_POWER_ON)) { + sendFrame(ORDER_POWER_ON, new byte[0]); + } else { + Log.i(TAG, "unhandled order=0x" + Integer.toHexString(code)); + } + } + + private void handleIdentity(byte[] body) { + if (body.length < 3) { + return; + } + int version = unsigned(body[0]); + if (version != 1) { + return; + } + int type = unsigned(body[1]); + int length = Math.min(unsigned(body[2]), body.length - 3); + String identity = new String(body, 3, Math.max(length, 0), GBK).trim(); + if (identity.startsWith("http") && identity.contains("card=")) { + identity = identity.substring(identity.indexOf("card=") + 5); + } + if (identity.matches("(?i)^[0-9a-f]{8}$")) { + type = 1; + } + if (identity.length() > 0 && listener != null) { + listener.onIdentity(type, identity); + } + } + + private void handleImei(byte[] body) { + if (body.length < 2) { + return; + } + int length = Math.min(unsigned(body[1]), body.length - 2); + String imei = new String(body, 2, Math.max(length, 0), GBK).trim(); + synchronized (imeiLock) { + lastImei = imei; + imeiLock.notifyAll(); + } + if (imei.length() > 0 && listener != null) { + listener.onImei(imei); + } + } + + private void handleVersion(byte[] body) { + if (body.length < 2) { + return; + } + int length = Math.min(unsigned(body[1]), body.length - 2); + String version = new String(body, 2, Math.max(length, 0), GBK).trim(); + synchronized (versionLock) { + lastVersion = version; + versionLock.notifyAll(); + } + if (version.length() > 0 && listener != null) { + listener.onVersion(version); + } + } + + private void handleThrowWeight(byte[] body) { + if (body.length < 2 || listener == null) { + Log.w(TAG, "invalid throw weight body length=" + body.length + " data=" + toHex(body)); + return; + } + int count = unsigned(body[1]); + int expectedLength = 2 + count * 8; + if (body.length < expectedLength) { + Log.w(TAG, "invalid throw weight body count=" + count + + " expected=" + expectedLength + " actual=" + body.length + + " data=" + toHex(body)); + return; + } + List items = new ArrayList<>(); + for (int i = 0; i < count; i += 1) { + int base = 2 + i * 8; + if (base + 7 >= body.length) { + break; + } + int port = unsigned(body[base]); + double weight = centWeight(body, base + 1); + double before = centWeight(body, base + 3); + double after = centWeight(body, base + 5); + int range = unsigned(body[base + 7]); + items.add(new ThrowWeightItem(port, weight, before, after, range, false)); + } + for (ThrowWeightItem item : items) { + listener.onThrowWeight(item); + } + } + + private void handleQueryWeight(byte[] body) { + if (body.length < 2 || listener == null) { + return; + } + int count = unsigned(body[1]); + for (int i = 0; i < count; i += 1) { + int base = 2 + i * 3; + if (base + 2 >= body.length) { + break; + } + int port = unsigned(body[base]); + double weight = centWeight(body, base + 1); + listener.onThrowWeight(new ThrowWeightItem(port, weight, weight, weight, -1, true)); + } + } + + private void handleQueryFill(byte[] body) { + if (body.length < 2 || listener == null) { + return; + } + int count = unsigned(body[1]); + for (int i = 0; i < count; i += 1) { + int base = 2 + i * 2; + if (base + 1 >= body.length) { + break; + } + listener.onDeviceStatus(new DeviceStatusItem(unsigned(body[base]), Double.NaN, unsigned(body[base + 1]))); + } + } + + private void handleQueryTemperature(byte[] body) { + if (body.length < 2 || listener == null) { + return; + } + int count = unsigned(body[1]); + for (int i = 0; i < count; i += 1) { + int base = 2 + i * 4; + if (base + 3 >= body.length) { + break; + } + listener.onDeviceStatus(new DeviceStatusItem(unsigned(body[base]), signedTemperature(body, base + 1), -1)); + } + } + + private void handleDeviceStatus(byte[] body) { + if (body.length < 2 || listener == null) { + return; + } + int count = unsigned(body[1]); + for (int i = 0; i < count; i += 1) { + int base = 2 + i * 5; + if (base + 4 >= body.length) { + break; + } + int port = unsigned(body[base]); + double temperature = signedTemperature(body, base + 1); + int range = unsigned(body[base + 4]); + listener.onDeviceStatus(new DeviceStatusItem(port, temperature, range)); + } + } + + private double centWeight(byte[] bytes, int index) { + int value = (unsigned(bytes[index]) << 8) | unsigned(bytes[index + 1]); + return new BigDecimal(value).divide(new BigDecimal(100)).doubleValue(); + } + + private double signedTemperature(byte[] bytes, int signIndex) { + int value = (unsigned(bytes[signIndex + 1]) << 8) | unsigned(bytes[signIndex + 2]); + BigDecimal temperature = new BigDecimal(value).divide(new BigDecimal(10)); + return bytes[signIndex] == 0x01 ? temperature.negate().doubleValue() : temperature.doubleValue(); + } + + private String normalizePath(String path) { + if (path == null || path.trim().length() == 0) { + return DEFAULT_PATH; + } + return path.trim(); + } + + private boolean hasRecentValidFrame() { + long at = lastValidFrameAt; + return at > 0 && System.currentTimeMillis() - at <= 15000; + } + + private boolean waitForValidFrameAfter(long baseline, long timeoutMs) { + long deadline = System.currentTimeMillis() + timeoutMs; + while (System.currentTimeMillis() < deadline) { + if (lastValidFrameAt > baseline) { + return true; + } + try { + Thread.sleep(40); + } catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); + return false; + } + } + return false; + } + + private static int unsigned(byte value) { + return value & 0xff; + } + + private static int clamp(int value, int min, int max) { + return Math.max(min, Math.min(max, value)); + } + + private static String toHex(byte[] data) { + return toHex(data, data.length); + } + + private static String toHex(byte[] data, int size) { + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < size; i += 1) { + if (i > 0) { + builder.append(' '); + } + int value = data[i] & 0xff; + if (value < 16) { + builder.append('0'); + } + builder.append(Integer.toHexString(value).toUpperCase(Locale.ROOT)); + } + return builder.toString(); + } +} diff --git a/app/src/main/java/com/zhitou/terminal/ZhitouSerialReadLoop.java b/app/src/main/java/com/zhitou/terminal/ZhitouSerialReadLoop.java new file mode 100644 index 0000000..309f7d0 --- /dev/null +++ b/app/src/main/java/com/zhitou/terminal/ZhitouSerialReadLoop.java @@ -0,0 +1,18 @@ +package com.xingyuan.zhiling; + +final class ZhitouSerialReadLoop implements Runnable { + private final ZhitouSerialPortManager manager; + private final long generation; + + ZhitouSerialReadLoop(ZhitouSerialPortManager manager, long generation) { + this.manager = manager; + this.generation = generation; + } + + @Override + public void run() { + while (manager.isRunning(generation)) { + manager.readSerialOnce(generation); + } + } +} diff --git a/app/src/main/java/com/zhitou/terminal/ZhitouSerialThrowWeightTask.java b/app/src/main/java/com/zhitou/terminal/ZhitouSerialThrowWeightTask.java new file mode 100644 index 0000000..52a7979 --- /dev/null +++ b/app/src/main/java/com/zhitou/terminal/ZhitouSerialThrowWeightTask.java @@ -0,0 +1,33 @@ +package com.xingyuan.zhiling; + +import android.webkit.WebView; + +import org.json.JSONObject; + +final class ZhitouSerialThrowWeightTask implements Runnable { + private final WebView webView; + private final ZhitouSerialPortManager.ThrowWeightItem item; + + ZhitouSerialThrowWeightTask(WebView webView, ZhitouSerialPortManager.ThrowWeightItem item) { + this.webView = webView; + this.item = item; + } + + @Override + public void run() { + JSONObject payload = new JSONObject(); + try { + payload.put("port", item.port); + payload.put("weight", item.weight); + payload.put("beforeWeight", item.beforeWeight); + payload.put("afterWeight", item.afterWeight); + payload.put("queryResponse", item.queryResponse); + if (item.range >= 0) { + payload.put("range", item.range); + } + } catch (Exception ignored) { + } + String js = "window.ZhitouSerial&&window.ZhitouSerial.onThrowWeight(" + payload.toString() + ");"; + webView.evaluateJavascript(js, null); + } +} diff --git a/app/src/main/java/com/zhitou/terminal/ZhitouSerialVersionRequestTask.java b/app/src/main/java/com/zhitou/terminal/ZhitouSerialVersionRequestTask.java new file mode 100644 index 0000000..31c05df --- /dev/null +++ b/app/src/main/java/com/zhitou/terminal/ZhitouSerialVersionRequestTask.java @@ -0,0 +1,24 @@ +package com.xingyuan.zhiling; + +import org.json.JSONObject; + +final class ZhitouSerialVersionRequestTask implements Runnable { + private final ZhitouSerialPortManager serialPortManager; + private final String json; + + ZhitouSerialVersionRequestTask(ZhitouSerialPortManager serialPortManager, String json) { + this.serialPortManager = serialPortManager; + this.json = json; + } + + @Override + public void run() { + try { + JSONObject data = json == null || json.length() == 0 ? new JSONObject() : new JSONObject(json); + serialPortManager.requestVersion( + data.optString("path", "/dev/ttyS4"), + data.optInt("baudRate", 9600)); + } catch (Exception ignored) { + } + } +} diff --git a/app/src/main/java/com/zhitou/terminal/ZhitouSerialVersionTask.java b/app/src/main/java/com/zhitou/terminal/ZhitouSerialVersionTask.java new file mode 100644 index 0000000..6097e61 --- /dev/null +++ b/app/src/main/java/com/zhitou/terminal/ZhitouSerialVersionTask.java @@ -0,0 +1,22 @@ +package com.xingyuan.zhiling; + +import android.webkit.WebView; + +import org.json.JSONObject; + +final class ZhitouSerialVersionTask implements Runnable { + private final WebView webView; + private final String version; + + ZhitouSerialVersionTask(WebView webView, String version) { + this.webView = webView; + this.version = version == null ? "" : version; + } + + @Override + public void run() { + String js = "window.ZhitouSerial&&window.ZhitouSerial.onVersion(" + + JSONObject.quote(version) + ");"; + webView.evaluateJavascript(js, null); + } +} diff --git a/app/src/main/java/com/zhitou/terminal/ZhitouTcpReporter.java b/app/src/main/java/com/zhitou/terminal/ZhitouTcpReporter.java new file mode 100644 index 0000000..16d7e20 --- /dev/null +++ b/app/src/main/java/com/zhitou/terminal/ZhitouTcpReporter.java @@ -0,0 +1,804 @@ +package com.xingyuan.zhiling; + +import android.content.Context; +import android.util.Log; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.InputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.math.BigDecimal; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketTimeoutException; +import java.nio.charset.Charset; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.Locale; + +final class ZhitouTcpReporter { + interface Listener { + void onReporterLog(String kind, String message); + } + + private static final String TAG = "ZhitouTcpReporter"; + private static final Charset UTF8 = Charset.forName("UTF-8"); + private static final Charset ASCII = Charset.forName("US-ASCII"); + private static final byte[] HEADER = "HERO".getBytes(ASCII); + private static final int ORDER_HEART = 1001; + private static final int ORDER_MEMBER_INFO = 4301; + private static final int ORDER_MEMBER_INFO_RESP = 4302; + private static final int ORDER_DEVICE_STATUS_2003 = 2003; + private static final int ORDER_THROW_INFO = 6011; + private static final int ORDER_ALARM_EVENT = 6006; + private static final int DEVICE_TYPE_GARBAGE_CAN = 0x01; + private static final int HEARTBEAT_INTERVAL_SECONDS = 30; + private static final int STATUS_INTERVAL_SECONDS = 10 * 60; + + private final Context context; + private final ExecutorService sender = Executors.newSingleThreadExecutor(); + private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + private final Object socketLock = new Object(); + private final Listener listener; + private final AtomicBoolean stopped = new AtomicBoolean(false); + private ScheduledFuture heartbeatFuture; + private ScheduledFuture statusFuture; + private Socket socket; + private InputStream inputStream; + private OutputStream outputStream; + private volatile String host = "112.74.41.228"; + private volatile int port = 9956; + private volatile long terminalId; + private long lastDeviceStatusSentAt; + private int serialNum = 1; + + ZhitouTcpReporter(Context context, Listener listener) { + this.context = context.getApplicationContext(); + this.listener = listener; + this.terminalId = 0L; + } + + synchronized void configure(String host, int port, String terminalIdText) { + if (stopped.get()) { + return; + } + boolean connectionChanged = false; + boolean terminalChanged = false; + if (host != null && host.trim().length() > 0) { + String nextHost = host.trim(); + if (!nextHost.equals(this.host)) { + this.host = nextHost; + connectionChanged = true; + } + } + if (port > 0) { + if (this.port != port) { + this.port = port; + connectionChanged = true; + } + } + if (terminalIdText != null) { + long parsed = parseTerminalId(terminalIdText); + long nextTerminalId = parsed > 0 ? parsed : 0L; + if (nextTerminalId != this.terminalId) { + this.terminalId = nextTerminalId; + connectionChanged = true; + terminalChanged = true; + } + } + if (connectionChanged) { + closeSocketQuietly(); + } + emitLog("传输", "配置 TCP " + this.host + ":" + this.port + " / IMEI " + getTerminalIdText()); + if (terminalChanged && this.terminalId > 0) { + sendHeartbeatNow(); + sendDeviceStatusNow(); + } + } + + synchronized void startHeartbeat() { + if (stopped.get()) { + return; + } + if (heartbeatFuture != null) { + heartbeatFuture.cancel(false); + } + sendHeartbeatNow(); + heartbeatFuture = scheduler.scheduleAtFixedRate( + new ZhitouHeartbeatTask(this), + HEARTBEAT_INTERVAL_SECONDS, + HEARTBEAT_INTERVAL_SECONDS, + TimeUnit.SECONDS); + startDeviceStatusReporter(); + emitLog("传输", "心跳任务已启动"); + } + + private synchronized void startDeviceStatusReporter() { + if (stopped.get()) { + return; + } + if (statusFuture != null) { + statusFuture.cancel(false); + } + sendDeviceStatusNow(); + statusFuture = scheduler.scheduleAtFixedRate( + new ZhitouDeviceStatusTask(this), + STATUS_INTERVAL_SECONDS, + STATUS_INTERVAL_SECONDS, + TimeUnit.SECONDS); + emitLog("传输", "设备状态任务已启动"); + } + + void stop() { + if (!stopped.compareAndSet(false, true)) { + return; + } + if (heartbeatFuture != null) { + heartbeatFuture.cancel(false); + } + if (statusFuture != null) { + statusFuture.cancel(false); + } + scheduler.shutdownNow(); + sender.shutdownNow(); + // connect() 最长可能阻塞 10 秒;退出 Activity 时异步收尾,避免主线程被 socketLock 卡住。 + Thread closer = new Thread(new Runnable() { + @Override + public void run() { + closeSocketQuietly(); + } + }, "zhitou-tcp-close"); + closer.setDaemon(true); + closer.start(); + } + + String getTerminalIdText() { + return terminalId > 0 ? String.valueOf(terminalId) : ""; + } + + void reportMemberVerify(int idType, String idContent) { + sendFrameAsync(ORDER_MEMBER_INFO, buildMemberVerifyBody(idType, idContent)); + } + + ZhitouMemberInfo requestMemberVerify(int idType, String idContent) { + if (stopped.get()) { + return ZhitouMemberInfo.failed("网络服务已停止"); + } + if (terminalId <= 0) { + emitLog("传输", "4301 跳过:未获取下位机IMEI"); + return ZhitouMemberInfo.failed("未获取下位机IMEI"); + } + byte[] body = buildMemberVerifyBody(idType, idContent); + byte[] frame = buildFrame(ORDER_MEMBER_INFO, body); + try { + synchronized (socketLock) { + ensureSocket(); + socket.setSoTimeout(500); + outputStream.write(frame); + outputStream.flush(); + emitTx(ORDER_MEMBER_INFO, frame); + long deadline = System.currentTimeMillis() + 8000; + while (System.currentTimeMillis() < deadline) { + ResponseFrame response = readResponseFrame(deadline); + if (response == null) { + continue; + } + emitRx(response); + if (response.orderCode == ORDER_MEMBER_INFO_RESP) { + return parseMemberInfo(response.body); + } + } + } + Log.w(TAG, "member verify timeout, closing socket and refreshing heartbeat"); + emitLog("传输", "4301 超时:未收到 4302"); + closeSocketQuietly(); + sendHeartbeatNow(); + return ZhitouMemberInfo.failed("会员资料返回超时"); + } catch (Exception error) { + closeSocketQuietly(); + Log.e(TAG, "member verify failed", error); + emitLog("传输", "4301 失败:" + error.getClass().getSimpleName()); + sendHeartbeatNow(); + return ZhitouMemberInfo.failed("会员鉴权通讯失败"); + } + } + + private byte[] buildMemberVerifyBody(int idType, String idContent) { + byte[] idBytes = fixedIdentityBytes(idType, idContent); + byte[] body = new byte[3 + 33]; + int index = 0; + body[index++] = (byte) DEVICE_TYPE_GARBAGE_CAN; + body[index++] = (byte) idType; + body[index++] = (byte) Math.min(rawIdentityLength(idType, idContent), 33); + System.arraycopy(idBytes, 0, body, index, 33); + return body; + } + + void reportDelivery( + int idType, + String idContent, + int throwIndex, + int garbageType, + int range, + double throwWeight, + double beforeWeight, + double afterWeight, + long createdAt) { + byte[] idBytes = fixedIdentityBytes(idType, idContent); + byte[] body = new byte[1 + 1 + 33 + 1 + 2 + 1 + 2 + 2]; + int index = 0; + body[index++] = (byte) idType; + body[index++] = (byte) Math.min(rawIdentityLength(idType, idContent), 33); + System.arraycopy(idBytes, 0, body, index, 33); + index += 33; + body[index++] = (byte) garbageType; + putUInt16(body, index, kgToCentWeight(throwWeight)); + index += 2; + body[index++] = (byte) clamp(range, 0, 255); + putUInt16(body, index, kgToCentWeight(beforeWeight)); + index += 2; + putUInt16(body, index, kgToCentWeight(afterWeight)); + sendFrameAsync(ORDER_THROW_INFO, body); + } + + void reportDeviceStatus(JSONObject data) { + sendDeviceStatus(data); + } + + void sendDeviceStatusNow() { + sendDeviceStatus(null); + } + + private synchronized void sendDeviceStatus(JSONObject data) { + if (terminalId <= 0) { + sendFrameAsync(ORDER_DEVICE_STATUS_2003, buildDeviceStatusBody(data)); + return; + } + long now = System.currentTimeMillis(); + if (lastDeviceStatusSentAt > 0 && now - lastDeviceStatusSentAt < 60 * 1000) { + emitLog("传输", "设备状态 2003 跳过:60秒内已上报"); + return; + } + lastDeviceStatusSentAt = now; + sendFrameAsync(ORDER_DEVICE_STATUS_2003, buildDeviceStatusBody(data)); + } + + private byte[] buildDeviceStatusBody(JSONObject data) { + if (data == null) { + data = new JSONObject(); + } + JSONArray ports = data.optJSONArray("ports"); + int count = ports == null ? 0 : ports.length(); + if (count == 0) { + count = 2; + } + byte[] body = new byte[19 + count * 2]; + int index = 0; + body[index++] = 0; + double temperature = data.optDouble("temperature", 0); + body[index++] = (byte) (temperature < 0 ? 1 : 0); + putUInt16(body, index, (int) Math.round(Math.abs(temperature) * 10)); + index += 2; + body[index++] = (byte) clamp(data.optInt("humidity", 0), 0, 255); + body[index++] = (byte) clamp(data.optInt("signal", 30), 0, 255); + body[index++] = hasLocation(data) ? (byte) 1 : (byte) 0; + putUInt32(body, index, decimal4(data.optString("lng", "0"))); + index += 4; + putUInt32(body, index, decimal4(data.optString("lat", "0"))); + index += 4; + putUInt32(body, index, decimal4(data.optString("altitude", "0"))); + index += 4; + for (int i = 0; i < count; i += 1) { + JSONObject item = ports == null ? null : ports.optJSONObject(i); + if (item == null) { + body[index++] = (byte) (i == 0 ? 3 : 1); + body[index++] = (byte) 150; + } else { + body[index++] = (byte) clamp(item.optInt("garbageType", 18), 0, 255); + body[index++] = (byte) clamp(item.optInt("range", 0), 0, 255); + } + } + return body; + } + + void reportAlarm(JSONArray alarms) { + if (alarms.length() == 0) return; + boolean hot = false; + for (int i = 0; i < alarms.length(); i += 1) { + JSONObject alarm = alarms.optJSONObject(i); + if (alarm != null && alarm.optInt("errorCode", 0) == 311) { + hot = true; + break; + } + } + byte[] body = new byte[(hot ? 7 : 5) * alarms.length() + 2]; + int index = 0; + body[index++] = 1; + body[index++] = (byte) alarms.length(); + for (int i = 0; i < alarms.length(); i += 1) { + JSONObject alarm = alarms.optJSONObject(i); + if (alarm == null) alarm = new JSONObject(); + int errorCode = alarm.optInt("errorCode", 301); + body[index++] = (byte) clamp(alarm.optInt("port", 0), 0, 255); + body[index++] = (byte) clamp(alarm.optInt("garbageType", 18), 0, 255); + putUInt16(body, index, errorCode); + index += 2; + body[index++] = (byte) clamp(alarm.optInt("isError", 1), 0, 1); + if (hot) { + if (errorCode == 311) { + putUInt16(body, index, (int) Math.round(alarm.optDouble("temperature", 0) * 10)); + } else { + putUInt16(body, index, 0); + } + index += 2; + } + } + sendFrameAsync(ORDER_ALARM_EVENT, body); + } + + void sendHeartbeatNow() { + byte[] body = new byte[1]; + body[0] = (byte) DEVICE_TYPE_GARBAGE_CAN; + sendFrameAsync(ORDER_HEART, body); + } + + private void sendFrameAsync(int orderCode, byte[] body) { + if (stopped.get()) { + return; + } + try { + sender.execute(new ZhitouSendFrameTask(this, orderCode, body)); + } catch (RejectedExecutionException ignored) { + // Activity 销毁与页面回调并发发生时,丢弃已经过期的上报任务。 + } + } + + void sendFrameNow(int orderCode, byte[] body) { + if (stopped.get()) { + return; + } + if (terminalId <= 0) { + Log.w(TAG, "skip send order=" + orderCode + ", terminal imei not ready"); + emitLog("传输", "TX " + orderCode + " 跳过:未获取下位机IMEI"); + return; + } + byte[] frame = buildFrame(orderCode, body); + try { + synchronized (socketLock) { + writeFrameLocked(frame); + emitTx(orderCode, frame); + drainResponsesLocked(350); + } + Log.i(TAG, "sent order=" + orderCode + " bytes=" + frame.length + " to " + host + ":" + port); + } catch (Exception error) { + closeSocketQuietly(); + Log.e(TAG, "send failed order=" + orderCode + " to " + host + ":" + port, error); + emitLog("传输", "TX " + orderCode + " 失败:" + errorSummary(error)); + retryFrameOnce(orderCode, frame); + } + } + + private void retryFrameOnce(int orderCode, byte[] frame) { + if (stopped.get()) { + return; + } + try { + synchronized (socketLock) { + writeFrameLocked(frame); + drainResponsesLocked(350); + } + Log.i(TAG, "retry sent order=" + orderCode + " bytes=" + frame.length + " to " + host + ":" + port); + emitLog("传输", "TX " + orderCode + " 重连重发成功 bytes=" + frame.length); + } catch (Exception retryError) { + closeSocketQuietly(); + Log.e(TAG, "retry send failed order=" + orderCode + " to " + host + ":" + port, retryError); + emitLog("传输", "TX " + orderCode + " 重连重发失败:" + errorSummary(retryError)); + } + } + + private void writeFrameLocked(byte[] frame) throws Exception { + ensureSocket(); + socket.setSoTimeout(500); + outputStream.write(frame); + outputStream.flush(); + } + + private void ensureSocket() throws Exception { + if (stopped.get()) { + throw new IOException("TCP reporter stopped"); + } + if (socket != null && socket.isConnected() && !socket.isClosed()) { + return; + } + closeSocketQuietly(); + Socket nextSocket = new Socket(); + nextSocket.setKeepAlive(true); + nextSocket.setTcpNoDelay(true); + nextSocket.setSoTimeout(500); + nextSocket.connect(new InetSocketAddress(host, port), 10000); + if (stopped.get()) { + try { + nextSocket.close(); + } catch (Exception ignored) { + } + throw new IOException("TCP reporter stopped"); + } + socket = nextSocket; + inputStream = nextSocket.getInputStream(); + outputStream = nextSocket.getOutputStream(); + Log.i(TAG, "connected to " + host + ":" + port); + emitLog("传输", "TCP 已连接 " + host + ":" + port); + } + + private void emitLog(String kind, String message) { + if (!stopped.get() && listener != null) { + listener.onReporterLog(kind, message); + } + } + + private void emitTx(int orderCode, byte[] frame) { + emitLog("传输", "TX " + orderCode + " bytes=" + frame.length + " sn=" + frameSerial(frame)); + emitLog("HEX", "TX " + orderCode + " " + toHex(frame, 160)); + Log.i(TAG, "TX " + orderCode + " " + toHex(frame, 160)); + } + + private void emitRx(ResponseFrame response) { + emitLog("传输", "RX " + response.orderCode + " body=" + response.body.length + " sn=" + response.serialNum); + emitLog("HEX", "RX " + response.orderCode + " " + toHex(response.fullFrame, 160)); + Log.i(TAG, "RX " + response.orderCode + " " + toHex(response.fullFrame, 160)); + } + + private String errorSummary(Exception error) { + if (error == null) { + return "未知错误"; + } + String message = error.getMessage(); + if (message == null || message.length() == 0) { + return error.getClass().getSimpleName(); + } + return error.getClass().getSimpleName() + " " + message; + } + + private void closeSocketQuietly() { + synchronized (socketLock) { + if (outputStream != null) { + try { + outputStream.close(); + } catch (Exception ignored) { + } + } + outputStream = null; + if (inputStream != null) { + try { + inputStream.close(); + } catch (Exception ignored) { + } + } + inputStream = null; + if (socket != null) { + try { + socket.close(); + } catch (Exception ignored) { + } + } + socket = null; + } + } + + private byte[] buildFrame(int orderCode, byte[] body) { + if (body == null) body = new byte[0]; + int currentSerial = nextSerialNum(); + int length = 8 + 2 + body.length + 2 + 1; + byte[] frame = new byte[HEADER.length + 2 + length]; + int index = 0; + System.arraycopy(HEADER, 0, frame, index, HEADER.length); + index += HEADER.length; + putUInt16(frame, index, length); + index += 2; + putUInt64(frame, index, terminalId); + index += 8; + putUInt16(frame, index, orderCode); + index += 2; + System.arraycopy(body, 0, frame, index, body.length); + index += body.length; + putUInt16(frame, index, currentSerial); + index += 2; + frame[index] = calcCrc(frame, HEADER.length + 2, 8 + 2 + body.length + 2); + return frame; + } + + private synchronized int nextSerialNum() { + int value = serialNum; + serialNum += 1; + if (serialNum > 65535) { + serialNum = 1; + } + return value; + } + + private void drainResponsesLocked(long waitMs) { + if (inputStream == null) { + return; + } + long deadline = System.currentTimeMillis() + Math.max(50, waitMs); + try { + while (System.currentTimeMillis() < deadline) { + ResponseFrame response = readResponseFrame(deadline); + if (response == null) { + return; + } + emitRx(response); + } + } catch (Exception error) { + emitLog("传输", "RX 读取异常:" + errorSummary(error)); + closeSocketQuietly(); + } + } + + private ResponseFrame readResponseFrame(long deadline) throws Exception { + if (inputStream == null) { + return null; + } + while (System.currentTimeMillis() < deadline) { + int first; + try { + first = inputStream.read(); + } catch (SocketTimeoutException timeout) { + return null; + } + if (first < 0) { + return null; + } + if (first != (HEADER[0] & 0xff)) { + continue; + } + if (!readRemainingHeader()) { + continue; + } + byte[] lengthBytes = new byte[2]; + readFully(lengthBytes, 0, 2); + int length = uint16(lengthBytes, 0); + if (length < 13 || length > 65535) { + continue; + } + byte[] payload = new byte[length]; + readFully(payload, 0, length); + byte[] full = new byte[HEADER.length + 2 + length]; + System.arraycopy(HEADER, 0, full, 0, HEADER.length); + full[HEADER.length] = lengthBytes[0]; + full[HEADER.length + 1] = lengthBytes[1]; + System.arraycopy(payload, 0, full, HEADER.length + 2, length); + byte expected = calcCrc(full, HEADER.length + 2, length - 1); + byte actual = payload[length - 1]; + if (expected != actual) { + Log.w(TAG, "response crc mismatch expected=" + (expected & 0xff) + " actual=" + (actual & 0xff)); + Log.w(TAG, "RX CRC mismatch frame=" + toHex(full, 160)); + emitLog("传输", "RX CRC错误 expected=" + (expected & 0xff) + " actual=" + (actual & 0xff)); + } + int orderCode = uint16(payload, 8); + int bodyLength = length - 8 - 2 - 2 - 1; + byte[] body = new byte[bodyLength]; + System.arraycopy(payload, 10, body, 0, bodyLength); + int serialNum = uint16(payload, 10 + bodyLength); + return new ResponseFrame(orderCode, body, serialNum, full); + } + return null; + } + + private boolean readRemainingHeader() throws Exception { + for (int i = 1; i < HEADER.length; i += 1) { + int value = inputStream.read(); + if (value != (HEADER[i] & 0xff)) { + return false; + } + } + return true; + } + + private void readFully(byte[] target, int offset, int length) throws Exception { + int read = 0; + while (read < length) { + int size; + try { + size = inputStream.read(target, offset + read, length - read); + } catch (SocketTimeoutException timeout) { + throw timeout; + } + if (size < 0) { + throw new java.io.IOException("socket closed"); + } + read += size; + } + } + + private ZhitouMemberInfo parseMemberInfo(byte[] data) { + if (data == null || data.length < 1) { + return ZhitouMemberInfo.failed("会员资料为空"); + } + ZhitouMemberInfo info = new ZhitouMemberInfo(); + info.verifyStatus = data[0] & 0xff; + if (info.verifyStatus == 0) { + info.error = "会员鉴权未通过"; + return info; + } + if (info.verifyStatus == 2) { + info.error = "用户已被禁用"; + return info; + } + if (info.verifyStatus != 1) { + info.error = "会员鉴权失败:" + info.verifyStatus; + return info; + } + if (data.length < 16) { + return ZhitouMemberInfo.failed("会员资料长度不足"); + } + info.memberId = String.valueOf(uint32(data, 1)); + info.memberCode = String.valueOf(uint32(data, 5)); + int nameLength = data[9] & 0xff; + if (10 + nameLength + 6 > data.length) { + return ZhitouMemberInfo.failed("会员资料字段长度异常"); + } + info.name = nameLength > 0 ? new String(data, 10, nameLength, UTF8) : ""; + info.resiCount = data[14 + nameLength] & 0xff; + long score = uint32(data, 10 + nameLength); + info.balance = formatMoney(score); + return info; + } + + private String formatMoney(long cents) { + return new BigDecimal(cents).movePointLeft(2).setScale(2).toPlainString(); + } + + private byte calcCrc(byte[] data, int offset, int length) { + byte crc = data[offset]; + for (int i = offset + 1; i < offset + length; i += 1) { + crc ^= data[i]; + } + return crc; + } + + private byte[] fixedIdentityBytes(int idType, String value) { + byte[] result = new byte[33]; + if (value == null) return result; + byte[] source = identityContentBytes(idType, value); + System.arraycopy(source, 0, result, 0, Math.min(source.length, result.length)); + return result; + } + + private int rawIdentityLength(int idType, String value) { + if (value == null) return 0; + return identityContentBytes(idType, value).length; + } + + private byte[] identityContentBytes(int idType, String value) { + if (value == null) { + return new byte[0]; + } + String text = value.trim(); + if (idType == 1 && text.matches("(?i)^[0-9a-f]{8}$")) { + byte[] bytes = new byte[4]; + for (int i = 0; i < bytes.length; i += 1) { + int start = i * 2; + bytes[i] = (byte) Integer.parseInt(text.substring(start, start + 2), 16); + } + return bytes; + } + return text.getBytes(UTF8); + } + + private int kgToCentWeight(double kg) { + return clamp((int) Math.round(kg * 100), 0, 65534); + } + + private int decimal4(String value) { + try { + return new BigDecimal(value).multiply(new BigDecimal(10000)).intValue(); + } catch (Exception ignored) { + return 0; + } + } + + private boolean hasLocation(JSONObject data) { + return decimal4(data.optString("lng", "0")) != 0 + || decimal4(data.optString("lat", "0")) != 0 + || decimal4(data.optString("altitude", "0")) != 0; + } + + private long parseTerminalId(String text) { + if (text == null || text.trim().length() == 0) { + return 0; + } + try { + return Long.parseLong(text.trim()); + } catch (Exception ignored) { + return 0; + } + } + + private int clamp(int value, int min, int max) { + if (value < min) return min; + if (value > max) return max; + return value; + } + + private int uint16(byte[] data, int offset) { + return ((data[offset] & 0xff) << 8) | (data[offset + 1] & 0xff); + } + + private int frameSerial(byte[] frame) { + if (frame == null || frame.length < HEADER.length + 2 + 8 + 2 + 2 + 1) { + return 0; + } + return uint16(frame, frame.length - 3); + } + + private String toHex(byte[] data, int maxBytes) { + if (data == null) { + return ""; + } + int limit = Math.min(data.length, Math.max(0, maxBytes)); + StringBuilder builder = new StringBuilder(limit * 3 + 16); + for (int i = 0; i < limit; i += 1) { + if (i > 0) builder.append(' '); + int value = data[i] & 0xff; + if (value < 16) builder.append('0'); + builder.append(Integer.toHexString(value).toUpperCase(Locale.ROOT)); + } + if (limit < data.length) { + builder.append(" ..."); + } + return builder.toString(); + } + + private long uint32(byte[] data, int offset) { + return ((long) (data[offset] & 0xff) << 24) + | ((long) (data[offset + 1] & 0xff) << 16) + | ((long) (data[offset + 2] & 0xff) << 8) + | (long) (data[offset + 3] & 0xff); + } + + private void putUInt16(byte[] data, int offset, int value) { + int v = clamp(value, 0, 65535); + data[offset] = (byte) ((v >> 8) & 0xff); + data[offset + 1] = (byte) (v & 0xff); + } + + private void putUInt32(byte[] data, int offset, int value) { + data[offset] = (byte) ((value >> 24) & 0xff); + data[offset + 1] = (byte) ((value >> 16) & 0xff); + data[offset + 2] = (byte) ((value >> 8) & 0xff); + data[offset + 3] = (byte) (value & 0xff); + } + + private void putUInt64(byte[] data, int offset, long value) { + data[offset] = (byte) ((value >>> 56) & 0xff); + data[offset + 1] = (byte) ((value >>> 48) & 0xff); + data[offset + 2] = (byte) ((value >>> 40) & 0xff); + data[offset + 3] = (byte) ((value >>> 32) & 0xff); + data[offset + 4] = (byte) ((value >>> 24) & 0xff); + data[offset + 5] = (byte) ((value >>> 16) & 0xff); + data[offset + 6] = (byte) ((value >>> 8) & 0xff); + data[offset + 7] = (byte) (value & 0xff); + } + + private static final class ResponseFrame { + final int orderCode; + final byte[] body; + final int serialNum; + final byte[] fullFrame; + + ResponseFrame(int orderCode, byte[] body, int serialNum, byte[] fullFrame) { + this.orderCode = orderCode; + this.body = body; + this.serialNum = serialNum; + this.fullFrame = fullFrame; + } + } +} diff --git a/app/src/main/java/com/zhitou/terminal/ZhitouWebChromeClient.java b/app/src/main/java/com/zhitou/terminal/ZhitouWebChromeClient.java new file mode 100644 index 0000000..af8f259 --- /dev/null +++ b/app/src/main/java/com/zhitou/terminal/ZhitouWebChromeClient.java @@ -0,0 +1,54 @@ +package com.xingyuan.zhiling; + +import android.content.ActivityNotFoundException; +import android.content.Intent; +import android.net.Uri; +import android.webkit.ValueCallback; +import android.webkit.WebChromeClient; +import android.webkit.PermissionRequest; +import android.webkit.WebView; +import android.widget.Toast; + +final class ZhitouWebChromeClient extends WebChromeClient { + private final MainActivity activity; + + ZhitouWebChromeClient(MainActivity activity) { + this.activity = activity; + } + + @Override + public void onPermissionRequest(final PermissionRequest request) { + if (request == null) { + return; + } + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + for (String resource : request.getResources()) { + if (PermissionRequest.RESOURCE_VIDEO_CAPTURE.equals(resource)) { + request.grant(new String[] { PermissionRequest.RESOURCE_VIDEO_CAPTURE }); + return; + } + } + request.deny(); + } + }); + } + + @Override + public boolean onShowFileChooser( + WebView view, + ValueCallback filePath, + FileChooserParams fileChooserParams) { + activity.setFilePathCallback(filePath); + Intent intent = fileChooserParams.createIntent(); + try { + activity.launchFileChooser(intent); + } catch (ActivityNotFoundException exception) { + activity.clearFilePathCallback(); + Toast.makeText(activity, "无法打开文件选择器", Toast.LENGTH_SHORT).show(); + return false; + } + return true; + } +} diff --git a/app/src/main/jniLibs/armeabi-v7a/libSeetaAuthorize.so b/app/src/main/jniLibs/armeabi-v7a/libSeetaAuthorize.so new file mode 100644 index 0000000..0e96c9a Binary files /dev/null and b/app/src/main/jniLibs/armeabi-v7a/libSeetaAuthorize.so differ diff --git a/app/src/main/jniLibs/armeabi-v7a/libSeetaFaceDetector600.so b/app/src/main/jniLibs/armeabi-v7a/libSeetaFaceDetector600.so new file mode 100644 index 0000000..d1e4811 Binary files /dev/null and b/app/src/main/jniLibs/armeabi-v7a/libSeetaFaceDetector600.so differ diff --git a/app/src/main/jniLibs/armeabi-v7a/libSeetaFaceLandmarker600.so b/app/src/main/jniLibs/armeabi-v7a/libSeetaFaceLandmarker600.so new file mode 100644 index 0000000..b11cc68 Binary files /dev/null and b/app/src/main/jniLibs/armeabi-v7a/libSeetaFaceLandmarker600.so differ diff --git a/app/src/main/jniLibs/armeabi-v7a/libSeetaFaceRecognizer600.so b/app/src/main/jniLibs/armeabi-v7a/libSeetaFaceRecognizer600.so new file mode 100644 index 0000000..c97e633 Binary files /dev/null and b/app/src/main/jniLibs/armeabi-v7a/libSeetaFaceRecognizer600.so differ diff --git a/app/src/main/jniLibs/armeabi-v7a/libTenniS.so b/app/src/main/jniLibs/armeabi-v7a/libTenniS.so new file mode 100644 index 0000000..277899a Binary files /dev/null and b/app/src/main/jniLibs/armeabi-v7a/libTenniS.so differ diff --git a/app/src/main/jniLibs/armeabi-v7a/libserial_port.so b/app/src/main/jniLibs/armeabi-v7a/libserial_port.so new file mode 100755 index 0000000..0ea7904 Binary files /dev/null and b/app/src/main/jniLibs/armeabi-v7a/libserial_port.so differ diff --git a/app/src/main/jniLibs/armeabi-v7a/libxingyuan_face.so b/app/src/main/jniLibs/armeabi-v7a/libxingyuan_face.so new file mode 100755 index 0000000..a7d9c45 Binary files /dev/null and b/app/src/main/jniLibs/armeabi-v7a/libxingyuan_face.so differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..37eb6f1 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..93147f7 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..7f260f7 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..e5ecd7f Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..e016d1b Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..b541425 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + 星元智灵 + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..bd44cc7 --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,9 @@ + + + diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..b506b87 --- /dev/null +++ b/build.sh @@ -0,0 +1,192 @@ +#!/bin/bash +# ============================================================ +# 星元智灵 APK 手动编译脚本 (无 Gradle) +# 使用 Android SDK 命令行工具: aapt / javac / d8 / zipalign / apksigner +# ============================================================ +set -e + +# ---- 路径配置 ---- +PROJECT="$(cd "$(dirname "$0")" && pwd)" +SDK="$HOME/Library/Android/sdk" +BT="$SDK/build-tools/34.0.0" +PLATFORM="$SDK/platforms/android-28/android.jar" +SRC="$PROJECT/app/src/main" +BUILD="$PROJECT/build" +KEYSTORE="$PROJECT/keystore/zhitou-debug.keystore" +APK_BASENAME="xingyuan-zhiling" +STORE_PASS="android" +KEY_PASS="android" +KEY_ALIAS="androiddebugkey" + +# ---- 工具路径 ---- +AAPT="$BT/aapt" +D8="$BT/d8" +ZIPALIGN="$BT/zipalign" +APKSIGNER="$BT/apksigner" +NDK_VERSION="27.3.13750724" +NDK="$SDK/ndk/$NDK_VERSION" +ARM_CLANGXX="$NDK/toolchains/llvm/prebuilt/darwin-x86_64/bin/armv7a-linux-androideabi21-clang++" + +# ---- 输出目录 ---- +OUT="$BUILD/out" +GEN="$BUILD/gen" +CLASSES="$BUILD/classes" +DEX="$BUILD/dex" + +echo "==========================================" +echo " 星元智灵 APK 编译" +echo "==========================================" +echo "SDK: $SDK" +echo "BuildTools: 34.0.0" +echo "Platform: android-28" +echo "Project: $PROJECT" +echo "" + +# ---- 恢复超过 Git 单文件限制的人脸模型 ---- +"$PROJECT/scripts/restore-face-models.sh" + +# ---- Debug 签名证书 ---- +if [ ! -f "$KEYSTORE" ]; then + echo "未找到 debug keystore,正在生成: $KEYSTORE" + mkdir -p "$(dirname "$KEYSTORE")" + keytool -genkeypair \ + -keystore "$KEYSTORE" \ + -storepass "$STORE_PASS" \ + -keypass "$KEY_PASS" \ + -alias "$KEY_ALIAS" \ + -keyalg RSA \ + -keysize 2048 \ + -validity 10000 \ + -dname "CN=Android Debug,O=Xingyuan Zhiling,C=CN" >/dev/null +fi + +# ---- Step 1: 清理 ---- +echo "[1/8] 清理旧的编译产物..." +rm -rf "$GEN" "$CLASSES" "$DEX" +mkdir -p "$GEN" "$CLASSES" "$DEX" "$OUT" + +# ---- Step 2: 生成 R.java ---- +echo "[2/8] 生成 R.java..." +"$AAPT" package -f -m \ + -J "$GEN" \ + -M "$SRC/AndroidManifest.xml" \ + -S "$SRC/res" \ + -A "$SRC/assets" \ + -I "$PLATFORM" + +# ---- Step 3: 收集 Java 源文件 ---- +echo "[3/8] 收集 Java 源文件..." +SRC_FILES=$(find "$SRC/java" "$GEN" -name "*.java" | tr '\n' ' ') +echo " 源文件数量: $(echo $SRC_FILES | wc -w | tr -d ' ')" + +# ---- Step 4: 编译 Java ---- +echo "[4/8] 编译 Java 源码..." +javac --release 8 -parameters \ + -classpath "$PLATFORM" \ + -sourcepath "$GEN" \ + -d "$CLASSES" \ + $SRC_FILES 2>&1 +echo " 编译完成" + +# ---- Step 5: 转换为 DEX ---- +echo "[5/8] 转换为 DEX..." +CLASS_FILES=$(find "$CLASSES" -name "*.class" | tr '\n' ' ') +"$D8" \ + --output "$DEX" \ + --lib "$PLATFORM" \ + $CLASS_FILES 2>&1 +echo " DEX 生成完成: $DEX/classes.dex" + +# ---- Step 6: 打包资源 + Manifest + Assets ---- +echo "[6/8] 打包 APK (资源 + Manifest + Assets)..." +rm -f "$OUT/$APK_BASENAME-unsigned.apk" "$OUT/$APK_BASENAME-aligned.apk" "$OUT/$APK_BASENAME-signed.apk" +"$AAPT" package -f \ + -M "$SRC/AndroidManifest.xml" \ + -S "$SRC/res" \ + -A "$SRC/assets" \ + -I "$PLATFORM" \ + -F "$OUT/$APK_BASENAME-unsigned.apk" + +# ---- Step 7: 添加 DEX 和 Native 库 ---- +echo "[7/8] 添加 DEX 和 Native 库到 APK..." +# 添加 classes.dex +cd "$DEX" +zip -j "$OUT/$APK_BASENAME-unsigned.apk" classes.dex +cd "$PROJECT" + +# 构建 SeetaFace6 JNI 桥(官方 Android 库就位后自动启用) +FACE_LIB_DIR="$SRC/jniLibs/armeabi-v7a" +FACE_JNI_SOURCE="$SRC/cpp/face_engine_jni.cpp" +FACE_JNI_OUTPUT="$FACE_LIB_DIR/libxingyuan_face.so" +FACE_REQUIRED_LIBS=( + libTenniS.so + libSeetaAuthorize.so + libSeetaFaceDetector600.so + libSeetaFaceLandmarker600.so + libSeetaFaceRecognizer600.so +) +FACE_LIBS_READY=true +for library in "${FACE_REQUIRED_LIBS[@]}"; do + if [ ! -f "$FACE_LIB_DIR/$library" ]; then + FACE_LIBS_READY=false + fi +done +if [ "$FACE_LIBS_READY" = true ] && [ -x "$ARM_CLANGXX" ]; then + echo " 编译 SeetaFace6 JNI 桥..." + "$ARM_CLANGXX" \ + --sysroot="$NDK/toolchains/llvm/prebuilt/darwin-x86_64/sysroot" \ + -std=c++11 -O2 -fPIC -shared -static-libstdc++ \ + -I"$SRC/cpp/include" \ + "$FACE_JNI_SOURCE" \ + -L"$FACE_LIB_DIR" \ + -lSeetaFaceDetector600 \ + -lSeetaFaceLandmarker600 \ + -lSeetaFaceRecognizer600 \ + -lSeetaAuthorize \ + -lTenniS \ + -llog -lz -ldl -lm \ + -o "$FACE_JNI_OUTPUT" +else + rm -f "$FACE_JNI_OUTPUT" + echo " ⚠ SeetaFace6 官方 Android 动态库尚未齐备,人脸 JNI 暂不打包" +fi + +# 添加 native 库 (APK 中需要 lib// 结构) +NATIVE_STAGING="$BUILD/native_staging" +rm -rf "$NATIVE_STAGING" +mkdir -p "$NATIVE_STAGING/lib/armeabi-v7a" +find "$SRC/jniLibs/armeabi-v7a" -maxdepth 1 -type f -name "*.so" -exec cp {} "$NATIVE_STAGING/lib/armeabi-v7a/" \; +cd "$NATIVE_STAGING" +zip -r "$OUT/$APK_BASENAME-unsigned.apk" lib/ +cd "$PROJECT" +echo " APK 打包完成" + +# ---- Step 8: Zipalign + 签名 ---- +echo "[8/8] Zipalign 并签名..." +# Zipalign +"$ZIPALIGN" -f 4 "$OUT/$APK_BASENAME-unsigned.apk" "$OUT/$APK_BASENAME-aligned.apk" + +# 签名 +"$APKSIGNER" sign \ + --ks "$KEYSTORE" \ + --ks-pass "pass:$STORE_PASS" \ + --ks-key-alias "$KEY_ALIAS" \ + --key-pass "pass:$KEY_PASS" \ + --out "$OUT/$APK_BASENAME-signed.apk" \ + "$OUT/$APK_BASENAME-aligned.apk" + +# 验证签名 +"$APKSIGNER" verify --verbose "$OUT/$APK_BASENAME-signed.apk" 2>&1 + +# ---- 完成 ---- +echo "" +echo "==========================================" +echo " ✅ 编译成功!" +echo "==========================================" +echo "" +ls -lh "$OUT/$APK_BASENAME-signed.apk" +echo "" +echo "输出文件: $OUT/$APK_BASENAME-signed.apk" +echo "" +echo "安装命令:" +echo " adb install -r $OUT/$APK_BASENAME-signed.apk" diff --git a/docs/星元智灵人脸识别回收箱-内网服务端接口规范-V1.0.docx b/docs/星元智灵人脸识别回收箱-内网服务端接口规范-V1.0.docx new file mode 100644 index 0000000..6c1d2de Binary files /dev/null and b/docs/星元智灵人脸识别回收箱-内网服务端接口规范-V1.0.docx differ diff --git a/docs/智能回收运营平台-Ubuntu24.04本地部署图文手册-V1.0.docx b/docs/智能回收运营平台-Ubuntu24.04本地部署图文手册-V1.0.docx new file mode 100644 index 0000000..ba9547f Binary files /dev/null and b/docs/智能回收运营平台-Ubuntu24.04本地部署图文手册-V1.0.docx differ diff --git a/management-console/.dockerignore b/management-console/.dockerignore new file mode 100644 index 0000000..87ffe45 --- /dev/null +++ b/management-console/.dockerignore @@ -0,0 +1,6 @@ +node_modules +.next +dist +.wrangler +.git +npm-debug.log diff --git a/management-console/.gitignore b/management-console/.gitignore new file mode 100644 index 0000000..236cfc6 --- /dev/null +++ b/management-console/.gitignore @@ -0,0 +1,42 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/.vinext/ +/out/ + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +next-env.d.ts +/dist/ +/.wrangler/ +/outputs/ +/work/ diff --git a/management-console/.openai/hosting.json b/management-console/.openai/hosting.json new file mode 100644 index 0000000..01e09ac --- /dev/null +++ b/management-console/.openai/hosting.json @@ -0,0 +1,4 @@ +{ + "d1": "DB", + "r2": null +} diff --git a/management-console/Dockerfile b/management-console/Dockerfile new file mode 100644 index 0000000..6a3c2f5 --- /dev/null +++ b/management-console/Dockerfile @@ -0,0 +1,13 @@ +FROM node:22-bookworm-slim AS builder +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci --ignore-scripts +COPY . . +RUN npm run build + +FROM node:22-bookworm-slim AS runtime +WORKDIR /app +ENV NODE_ENV=production +COPY --from=builder /app ./ +EXPOSE 3000 +CMD ["npm", "run", "start"] diff --git a/management-console/README.md b/management-console/README.md new file mode 100644 index 0000000..e3ef761 --- /dev/null +++ b/management-console/README.md @@ -0,0 +1,55 @@ +# 智能回收运营平台 + +面向工厂内网部署的称重与设备管理后台,按公开演示后台的业务范围重新实现,不复用对方代码或视觉素材。 + +## 已实现 + +- 工作台:投放量、称重次数、待核对数据、设备在线率、最近流水 +- 废料称重:记录查询、统计、CSV 导出 +- 事件列表:按投放事件聚合原子称重记录 +- 地磅核对:台秤与地磅偏差判定 +- 导体追溯:RFID、批次、皮重、毛重、净重、MES 状态 +- 人员设备:人员、人脸版本、车间、设备心跳、满溢率 +- 告警中心:接单、关闭、阈值配置 +- 系统设置:角色、车间、设备监控、MES、审计日志 +- 终端模拟:废料投放与导体称重流程演示 +- 设备接口:Token、投放、心跳、状态、告警、人员拉取、人脸查询、同步回执 + +## 本机开发 + +需要 Node.js 22 或更高版本: + +```bash +npm ci +npm run dev +``` + +浏览器访问 `http://localhost:3000`。 + +## Ubuntu 24.04 部署 + +推荐使用 Docker Compose: + +```bash +docker compose up -d --build +``` + +后台地址:`http://服务器IP:3000`。数据库持久化在 Docker 卷 `console-data` 中。 + +首次部署后建议在路由器或防火墙中只允许内网访问,并通过 Nginx 配置 HTTPS。 + +## 设备接口示例 + +```text +POST /api/v1/auth/tokens +POST /api/v1/devices/{deviceId}/deliveries +POST /api/v1/devices/{deviceId}/heartbeat +POST /api/v1/devices/{deviceId}/status +POST /api/v1/devices/{deviceId}/alerts +GET /api/v1/devices/{deviceId}/people/incremental?cursor=0 +GET /api/v1/devices/{deviceId}/faces/{personId} +POST /api/v1/devices/{deviceId}/sync-acks +GET /api/v1/deliveries/{recordId} +``` + +当前版本用于本地联调。正式投产前还需要补充设备密钥、签名校验、管理员登录和自动备份。 diff --git a/management-console/app/AdminConsole.tsx b/management-console/app/AdminConsole.tsx new file mode 100644 index 0000000..f08dbcb --- /dev/null +++ b/management-console/app/AdminConsole.tsx @@ -0,0 +1,820 @@ +"use client"; + +import { FormEvent, ReactNode, useEffect, useMemo, useState } from "react"; + +type Row = Record; +type ConsoleData = { + people: Row[]; + workshops: Row[]; + devices: Row[]; + weighings: Row[]; + events: Row[]; + reconciliations: Row[]; + traces: Row[]; + tags: Row[]; + alerts: Row[]; + settings: Record; + logs: Row[]; + serverTime: number; +}; + +type PageKey = + | "dashboard" + | "weighings" + | "events" + | "reconcile" + | "trace" + | "assets" + | "alerts" + | "settings" + | "simulator"; + +type RoleKey = "warehouse" | "worker" | "admin"; + +const pageMeta: Record = { + dashboard: { name: "工作台", en: "Dashboard", icon: "◫" }, + weighings: { name: "废料称重", en: "Weighing records", icon: "▦" }, + events: { name: "事件列表", en: "Events", icon: "◎" }, + reconcile: { name: "地磅核对", en: "Reconciliation", icon: "⚖" }, + trace: { name: "导体追溯", en: "Cable trace", icon: "⌁" }, + assets: { name: "人员设备", en: "People & devices", icon: "♙" }, + alerts: { name: "告警中心", en: "Alerts", icon: "!" }, + settings: { name: "系统设置", en: "Settings", icon: "⌘" }, + simulator: { name: "终端模拟", en: "Terminal simulator", icon: "▣" }, +}; + +const roleMeta: Record = { + warehouse: { + name: "仓管员", + person: "张三", + pages: ["dashboard", "weighings", "events", "reconcile", "assets", "simulator"], + }, + worker: { + name: "产线工人", + person: "李四", + pages: ["dashboard", "trace", "assets", "simulator"], + }, + admin: { + name: "管理员", + person: "王工", + pages: Object.keys(pageMeta) as PageKey[], + }, +}; + +const navGroups: Array<{ title: string; pages: PageKey[] }> = [ + { title: "总览", pages: ["dashboard"] }, + { title: "废料库回收", pages: ["weighings", "events", "reconcile"] }, + { title: "导体称重", pages: ["trace"] }, + { title: "运营管理", pages: ["assets", "alerts"] }, + { title: "系统", pages: ["settings"] }, + { title: "演示工具", pages: ["simulator"] }, +]; + +const n = (row: Row, key: string) => Number(row[key] ?? 0); +const s = (row: Row, key: string) => String(row[key] ?? ""); + +function formatTime(value: string | number | null, includeDate = false) { + const date = new Date(Number(value ?? 0)); + if (Number.isNaN(date.getTime())) return "—"; + return new Intl.DateTimeFormat("zh-CN", { + ...(includeDate ? { month: "2-digit", day: "2-digit" } : {}), + hour: "2-digit", + minute: "2-digit", + hour12: false, + }).format(date); +} + +function Status({ value }: { value: string }) { + const level = + /超标|严重|失败|离线|停用|损坏|异常/.test(value) + ? "bad" + : /警告|预警|处理中|待推送|需更新|未用/.test(value) + ? "warn" + : /在线|正常|成功|已同步|已确认|已录入|在用|已自动|已推送/.test(value) + ? "ok" + : "muted"; + return {value}; +} + +function Card({ + title, + subtitle, + tools, + children, + className = "", +}: { + title?: string; + subtitle?: string; + tools?: ReactNode; + children: ReactNode; + className?: string; +}) { + return ( +
+ {(title || tools) && ( +
+
+ {title &&

{title}

} + {subtitle &&

{subtitle}

} +
+ {tools &&
{tools}
} +
+ )} + {children} +
+ ); +} + +function DataTable({ + columns, + rows, +}: { + columns: Array<{ key: string; label: string; render?: (row: Row) => ReactNode }>; + rows: Row[]; +}) { + return ( +
+ + + {columns.map((column) => )} + + + {rows.length === 0 ? ( + + ) : rows.map((row, index) => ( + + {columns.map((column) => ( + + ))} + + ))} + +
{column.label}
没有符合条件的数据
{column.render ? column.render(row) : String(row[column.key] ?? "—")}
+
+ ); +} + +function SearchBox({ + value, + onChange, + placeholder = "搜索…", +}: { + value: string; + onChange: (value: string) => void; + placeholder?: string; +}) { + return ( + + ); +} + +function exportCsv(filename: string, rows: Row[]) { + if (!rows.length) return; + const headers = Object.keys(rows[0]); + const escape = (value: unknown) => `"${String(value ?? "").replaceAll('"', '""')}"`; + const csv = "\ufeff" + [headers.map(escape).join(","), ...rows.map((row) => headers.map((key) => escape(row[key])).join(","))].join("\n"); + const url = URL.createObjectURL(new Blob([csv], { type: "text/csv;charset=utf-8" })); + const anchor = document.createElement("a"); + anchor.href = url; + anchor.download = filename; + anchor.click(); + URL.revokeObjectURL(url); +} + +export function AdminConsole() { + const [data, setData] = useState(null); + const [error, setError] = useState(""); + const [loading, setLoading] = useState(true); + const [page, setPage] = useState("dashboard"); + const [role, setRole] = useState("admin"); + const [query, setQuery] = useState(""); + const [tab, setTab] = useState("records"); + const [toast, setToast] = useState(""); + const [clock, setClock] = useState(new Date()); + const [modal, setModal] = useState<"workshop" | "tag" | "threshold" | null>(null); + const [simStep, setSimStep] = useState(0); + + async function load() { + setLoading(true); + try { + const response = await fetch("/api/console", { cache: "no-store" }); + const payload = await response.json() as { code: number; message?: string; data?: ConsoleData }; + if (!response.ok || !payload.data) throw new Error(payload.message || "数据加载失败"); + setData(payload.data); + setError(""); + } catch (cause) { + setError(cause instanceof Error ? cause.message : "数据加载失败"); + } finally { + setLoading(false); + } + } + + useEffect(() => { + load(); + const timer = window.setInterval(() => setClock(new Date()), 1_000); + return () => window.clearInterval(timer); + }, []); + + useEffect(() => { + setQuery(""); + if (page === "trace") setTab("trace"); + else if (page === "assets") setTab("people"); + else if (page === "settings") setTab("accounts"); + else if (page === "weighings") setTab("records"); + }, [page]); + + async function action(name: string, payload: Record = {}, message = "操作成功") { + try { + const response = await fetch("/api/console", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ action: name, payload }), + }); + const result = await response.json() as { code: number; message?: string; data?: ConsoleData }; + if (!response.ok || result.code !== 0) throw new Error(result.message || "操作失败"); + if (result.data) setData(result.data); + setToast(message); + window.setTimeout(() => setToast(""), 2_800); + } catch (cause) { + setToast(cause instanceof Error ? cause.message : "操作失败"); + window.setTimeout(() => setToast(""), 3_600); + } + } + + function switchRole(nextRole: RoleKey) { + setRole(nextRole); + if (!roleMeta[nextRole].pages.includes(page)) setPage("dashboard"); + setToast(`已切换为${roleMeta[nextRole].name}视角`); + window.setTimeout(() => setToast(""), 2_200); + } + + if (loading && !data) { + return ( +
+
+

智能回收运营平台

+

正在连接本地数据服务…

+
+ ); + } + + if (!data) { + return ( +
+
!
+

数据服务暂不可用

+

{error}

+ +
+ ); + } + + const visiblePages = roleMeta[role].pages; + const openAlerts = data.alerts.filter((item) => s(item, "status") !== "已关闭").length; + const onlineDevices = data.devices.filter((item) => s(item, "status") === "在线").length; + const title = pageMeta[page]; + + return ( +
+ + +
+
+
+

{title.name}

+ {title.en} +
+
+
服务正常
+
+ {clock.toLocaleTimeString("zh-CN", { hour12: false })} + {clock.toLocaleDateString("zh-CN", { month: "2-digit", day: "2-digit", weekday: "short" })} +
+ +
+ {(Object.keys(roleMeta) as RoleKey[]).map((key) => ( + + ))} +
+
+
+ +
+ {page === "dashboard" && } + {page === "weighings" && ( + + )} + {page === "events" && } + {page === "reconcile" && } + {page === "trace" && ( + setModal("tag")} /> + )} + {page === "assets" && ( + + )} + {page === "alerts" && setModal("threshold")} />} + {page === "settings" && ( + setModal("workshop")} /> + )} + {page === "simulator" && } +
+
+ + {modal && ( + setModal(null)} + onSubmit={async (values) => { + if (modal === "workshop") await action("workshop.add", values, "车间已新增"); + if (modal === "tag") await action("tag.add", values, "RFID 标签已新增"); + if (modal === "threshold") await action("settings.save", values, "告警阈值已保存"); + setModal(null); + }} + /> + )} + {toast &&
{toast}
} +
+ ); +} + +function Dashboard({ data, role, setPage }: { data: ConsoleData; role: RoleKey; setPage: (page: PageKey) => void }) { + const todayTotal = data.weighings.reduce((total, row) => total + n(row, "weight"), 0); + const netTotal = data.traces.reduce((total, row) => total + n(row, "net"), 0); + const pending = data.reconciliations.filter((row) => s(row, "verdict") !== "正常").length; + const deviceOnline = data.devices.filter((row) => s(row, "status") === "在线").length; + const showWaste = role !== "worker"; + const kpis = showWaste + ? [ + ["今日废料投放", `${todayTotal.toFixed(1)} kg`, "按已接收记录实时汇总", "orange"], + ["称重次数", `${data.weighings.length} 次`, "原子称重记录", "blue"], + ["待核对", `${pending} 条`, "警告与超标记录", "amber"], + ["设备在线率", `${deviceOnline}/${data.devices.length}`, "最近心跳状态", "green"], + ] + : [ + ["今日称重轴数", `${data.traces.length} 轴`, "导体追溯记录", "blue"], + ["净重累计", `${netTotal.toFixed(1)} kg`, "毛重减皮重", "green"], + ["MES 待推送", `${data.traces.filter((row) => s(row, "mes_status") !== "已推送").length} 条`, "自动重试队列", "amber"], + ["RFID 标签", `${data.tags.length} 个`, "在用与备用标签", "orange"], + ]; + return ( +
+
+
+ 你好,{roleMeta[role].person} +

{roleMeta[role].name}视角 · 今日运营概览

+
+ +
+
+ {kpis.map(([label, value, note, color]) => ( +
+ {label} + {value} + {note} +
+ ))} +
+
+ + {showWaste ? ( +
+
+ 12.48kg +

采样稳定 · 精度 ±10g

+
+
+
操作人
张三
+
机台
MC-101
+
类别
废铜
+
来源
小秤台
+
+
+ ) : ( + `${n(row, "net").toFixed(1)} kg` }, + { key: "mes_status", label: "MES", render: (row) => }, + ]} + /> + )} +
+ +
+ {data.devices.slice(0, 6).map((device) => ( +
+ + {s(device, "id")}{s(device, "type")} + {formatTime(device.last_seen)} +
+ ))} +
+
+
+
+ +
+ {[54, 72, 61, 86, 66, 78, 93].map((height, index) => ( +
{["一", "二", "三", "四", "五", "六", "日"][index]}
+ ))} +
+
+ +
+ {data.weighings.slice(0, 5).map((row) => ( +
+ + {s(row, "operator")} · {s(row, "machine")} · {s(row, "category")} + {n(row, "weight").toFixed(2)} kg +
+ ))} +
+
+
+
+ ); +} + +function Weighings({ data, query, setQuery, tab, setTab }: { data: ConsoleData; query: string; setQuery: (value: string) => void; tab: string; setTab: (value: string) => void }) { + const rows = useMemo(() => { + const key = query.toLowerCase(); + return data.weighings.filter((row) => !key || [row.operator, row.machine, row.category, row.event_id].some((value) => String(value ?? "").toLowerCase().includes(key))); + }, [data.weighings, query]); + const categoryTotals = ["废铜", "废电线", "废铝", "废锡"].map((category) => ({ + category, + value: rows.filter((row) => s(row, "category") === category).reduce((total, row) => total + n(row, "weight"), 0), + })); + return ( + exportCsv("废料称重记录.csv", rows)}>导出 CSV} + > +
+
+ + +
+ +
+ {tab === "records" ? ( + formatTime(row.created_at, true) }, + { key: "operator", label: "操作人" }, + { key: "machine", label: "关联机台" }, + { key: "category", label: "废料类别" }, + { key: "source", label: "来源设备" }, + { key: "weight", label: "精确重量", render: (row) => {n(row, "weight").toFixed(2)} kg }, + { key: "event_id", label: "所属事件" }, + { key: "status", label: "状态", render: (row) => }, + ]} + /> + ) : ( +
+ {categoryTotals.map((item) => ( +
+ {item.category} + {item.value.toFixed(1)} kg + +
+ ))} +
+ )} +
+ ); +} + +function EventsPage({ data, query, setQuery }: { data: ConsoleData; query: string; setQuery: (value: string) => void }) { + const rows = data.events.filter((row) => !query || `${s(row, "id")}${s(row, "operator")}`.toLowerCase().includes(query.toLowerCase())); + return ( + exportCsv("事件列表.csv", rows)}>导出 CSV}> +
+ formatTime(row.entered_at, true) }, + { key: "left_at", label: "离门时间", render: (row) => formatTime(row.left_at) }, + { key: "duration", label: "历时", render: (row) => `${Math.max(1, Math.round((n(row, "left_at") - n(row, "entered_at")) / 60_000))} 分钟` }, + { key: "atomic_count", label: "原子笔数" }, + { key: "scale_total", label: "台秤总重", render: (row) => `${n(row, "scale_total").toFixed(2)} kg` }, + { key: "verdict", label: "判定", render: (row) => }, + ]} /> +
+ ); +} + +function Reconcile({ data }: { data: ConsoleData }) { + return ( + exportCsv("地磅核对.csv", data.reconciliations)}>导出 CSV}> + `${n(row, "small_scale").toFixed(2)} kg` }, + { key: "floor_scale", label: "地磅增量", render: (row) => `${n(row, "floor_scale").toFixed(2)} kg` }, + { key: "difference", label: "偏差值", render: (row) => `${n(row, "difference").toFixed(2)} kg` }, + { key: "difference_rate", label: "偏差率", render: (row) => `${n(row, "difference_rate").toFixed(2)}%` }, + { key: "verdict", label: "判定", render: (row) => }, + { key: "storage_status", label: "入库状态", render: (row) => }, + ]} /> + + ); +} + +function TracePage({ data, query, setQuery, tab, setTab, action, openTag }: { data: ConsoleData; query: string; setQuery: (value: string) => void; tab: string; setTab: (value: string) => void; action: (name: string, payload?: Record, message?: string) => Promise; openTag: () => void }) { + const rows = data.traces.filter((row) => !query || `${s(row, "task_id")}${s(row, "rfid")}${s(row, "batch")}`.toLowerCase().includes(query.toLowerCase())); + return ( + +
+
+ + +
+ {tab === "trace" ? : } +
+ {tab === "trace" ? formatTime(row.created_at, true) }, + { key: "workshop", label: "所属车间" }, + { key: "task_id", label: "任务号" }, + { key: "rfid", label: "RFID 标签" }, + { key: "tare", label: "皮重", render: (row) => `${n(row, "tare").toFixed(2)} kg` }, + { key: "gross", label: "毛重", render: (row) => `${n(row, "gross").toFixed(2)} kg` }, + { key: "net", label: "净重", render: (row) => {n(row, "net").toFixed(2)} kg }, + { key: "machine", label: "机台" }, + { key: "batch", label: "批次号" }, + { key: "mes_status", label: "MES 推送", render: (row) => }, + ]} /> : }, + { key: "name", label: "名称" }, + { key: "last_checked", label: "最后检测", render: (row) => row.last_checked ? formatTime(row.last_checked, true) : "—" }, + { key: "actions", label: "操作", render: (row) => ( +
+ + +
+ ) }, + ]} />} +
+ ); +} + +function Assets({ data, query, setQuery, tab, setTab, role, action }: { data: ConsoleData; query: string; setQuery: (value: string) => void; tab: string; setTab: (value: string) => void; role: RoleKey; action: (name: string, payload?: Record, message?: string) => Promise }) { + const people = data.people.filter((row) => !query || `${s(row, "name")}${s(row, "id")}`.toLowerCase().includes(query.toLowerCase())); + return ( + +
+
+ + + +
+ {tab === "people" && } + {tab === "people" && role === "admin" && } +
+ {tab === "people" && role === "admin" ? ( + + ) : s(row, "workshop") }, + { key: "role", label: "角色" }, + { key: "face_status", label: "人脸状态", render: (row) => }, + { key: "sync_status", label: "同步状态", render: (row) => }, + { key: "active", label: "账号状态", render: (row) => }, + { key: "actions", label: "操作", render: (row) => role === "admin" ? ( +
+ + +
+ ) : "只读" }, + ]} />} + {tab === "machines" && }, + { key: "mes", label: "MES 同步", render: (row) => }, + ]} />} + {tab === "devices" && ( +
+ {data.devices.map((device) => ( +
+
{s(device, "id")}
+

{s(device, "type")}

+

{s(device, "ip")} · 心跳 {s(device, "heartbeat_seconds")}s

+ {n(device, "fill_percent") > 0 && ( + <> +
+ 满溢率 {n(device, "fill_percent")}% · {s(device, "category")} + + )} +
最后心跳 {formatTime(device.last_seen)}
+
+ ))} +
+ )} +
+ ); +} + +function AlertsPage({ data, role, action, openThreshold }: { data: ConsoleData; role: RoleKey; action: (name: string, payload?: Record, message?: string) => Promise; openThreshold: () => void }) { + return ( + 阈值设置}> +
+ {data.alerts.map((alert) => ( +
+
!
+
+

{s(alert, "type")}

+

{s(alert, "description")}

+ {formatTime(alert.created_at, true)} · {s(alert, "source")} · {s(alert, "reference")} +
+ {role === "admin" && s(alert, "status") !== "已关闭" && ( +
+ + +
+ )} +
+ ))} +
+
+ ); +} + +function SettingsPage({ data, tab, setTab, action, openWorkshop }: { data: ConsoleData; tab: string; setTab: (value: string) => void; action: (name: string, payload?: Record, message?: string) => Promise; openWorkshop: () => void }) { + const tabs = [["accounts", "账号与权限"], ["workshops", "车间字典"], ["monitor", "设备监控"], ["mes", "MES 接口"], ["logs", "操作日志"]]; + return ( + +
{tabs.map(([key, label]) => )}
+ {tab === "accounts" && } + {tab === "workshops" && ( + <> +
车间主数据字典
+ `${data.people.filter((person) => s(person, "workshop") === s(row, "name")).length} 人` }, + { key: "actions", label: "操作", render: (row) =>
}, + ]} /> + + )} + {tab === "monitor" && `${s(row, "heartbeat_seconds")} 秒` }, + { key: "last_seen", label: "最后心跳", render: (row) => formatTime(row.last_seen, true) }, + { key: "status", label: "状态", render: (row) => }, + ]} />} + {tab === "mes" && } + {tab === "logs" && formatTime(row.created_at, true) }, + { key: "actor", label: "操作人" }, { key: "action", label: "动作" }, { key: "details", label: "详情" }, + { key: "level", label: "级别", render: (row) => }, + ]} />} +
+ ); +} + +function MesSettings({ settings, action }: { settings: Record; action: (name: string, payload?: Record, message?: string) => Promise }) { + const [url, setUrl] = useState(settings.mes_url ?? ""); + const [timeout, setTimeoutValue] = useState(settings.mes_timeout ?? "10"); + const [testing, setTesting] = useState(false); + return ( +
+ + + +
+ + + {!testing && } +
+
+ ); +} + +function Simulator({ step, setStep }: { step: number; setStep: (value: number) => void }) { + const wasteSteps = [ + ["身份验证", "请将面部对准摄像头,也可输入工号", "模拟识别成功 · 张三"], + ["门锁已开启", "请进入投放,最长停留 05:00", "模拟投放完成并关门"], + ["投放完成", "台秤 38.50kg · 地磅 39.00kg · 偏差 1.3%", "重新开始"], + ]; + const cableSteps = [ + ["导体轴称重", "RFID 全程关联生产任务与批次", "开始一次称重"], + ["一次称重完成", "RFID E280-6102-0345 · 皮重 152.30kg", "模拟二次称重"], + ["MES 上报成功", "净重 115.80kg · 条码已打印", "重新开始"], + ]; + return ( +
+ {[["废料库入库终端", wasteSteps, "orange"], ["导体称重终端", cableSteps, "blue"]].map(([name, steps, color], index) => { + const items = steps as string[][]; + const current = index === 0 ? step % 3 : Math.floor(step / 3) % 3; + return ( + + + + ); + })} +
+ ); +} + +function Modal({ type, settings, onClose, onSubmit }: { type: "workshop" | "tag" | "threshold"; settings: Record; onClose: () => void; onSubmit: (values: Record) => Promise }) { + const [saving, setSaving] = useState(false); + async function submit(event: FormEvent) { + event.preventDefault(); + setSaving(true); + const values = Object.fromEntries(new FormData(event.currentTarget).entries()) as Record; + await onSubmit(values); + setSaving(false); + } + return ( +
{ if (event.currentTarget === event.target) onClose(); }}> +
+

{type === "workshop" ? "新增车间" : type === "tag" ? "添加 RFID 标签" : "告警阈值设置"}

+
+ {type === "workshop" && <> + + + + } + {type === "tag" && } + {type === "threshold" && <> + + + + + } +
+
+
+
+ ); +} diff --git a/management-console/app/api/console/route.ts b/management-console/app/api/console/route.ts new file mode 100644 index 0000000..8d85cf6 --- /dev/null +++ b/management-console/app/api/console/route.ts @@ -0,0 +1,93 @@ +import { addAudit, ensureDatabase, loadConsoleData } from "@/db/store"; + +export const dynamic = "force-dynamic"; + +export async function GET() { + try { + return Response.json({ code: 0, data: await loadConsoleData() }); + } catch (error) { + return Response.json( + { code: 5001, message: error instanceof Error ? error.message : "数据库不可用" }, + { status: 500 }, + ); + } +} + +export async function POST(request: Request) { + const db = await ensureDatabase(); + const body = (await request.json()) as { + action?: string; + payload?: Record; + }; + const payload = body.payload ?? {}; + const action = body.action ?? ""; + + if (action === "person.toggle") { + const id = String(payload.id ?? ""); + await db + .prepare("UPDATE people SET active=CASE active WHEN 1 THEN 0 ELSE 1 END, updated_at=? WHERE id=?") + .bind(Date.now(), id) + .run(); + await addAudit("管理员", "人员状态变更", `${id} 启用状态已切换`, "重要"); + } else if (action === "person.workshop") { + const id = String(payload.id ?? ""); + const workshop = String(payload.workshop ?? ""); + await db.prepare("UPDATE people SET workshop=?, updated_at=? WHERE id=?").bind(workshop, Date.now(), id).run(); + await addAudit("管理员", "修改人员车间", `${id} 调整到 ${workshop}`); + } else if (action === "person.face") { + const id = String(payload.id ?? ""); + await db.prepare("UPDATE people SET face_status='已录入', face_version=face_version+1, sync_status='已同步', updated_at=? WHERE id=?").bind(Date.now(), id).run(); + await addAudit("管理员", "更新人脸", `${id} 人脸版本已更新`, "重要"); + } else if (action === "workshop.add") { + const name = String(payload.name ?? "").trim(); + const location = String(payload.location ?? "—").trim(); + const note = String(payload.note ?? "—").trim(); + if (!name) return Response.json({ code: 1001, message: "车间名称不能为空" }, { status: 400 }); + const row = await db.prepare("SELECT COUNT(*) AS total FROM workshops").first<{ total: number }>(); + const code = `WS-${String(Number(row?.total ?? 0) + 1).padStart(2, "0")}`; + await db.prepare("INSERT INTO workshops(code,name,location,note) VALUES(?,?,?,?)").bind(code, name, location || "—", note || "—").run(); + await addAudit("管理员", "新增车间", `${code} ${name}`, "重要"); + } else if (action === "workshop.delete") { + const code = String(payload.code ?? ""); + const workshop = await db.prepare("SELECT name FROM workshops WHERE code=?").bind(code).first<{ name: string }>(); + if (workshop) { + const used = await db.prepare("SELECT COUNT(*) AS total FROM people WHERE workshop=?").bind(workshop.name).first<{ total: number }>(); + if (Number(used?.total ?? 0) > 0) { + return Response.json({ code: 4001, message: "该车间仍有关联人员,不能删除" }, { status: 409 }); + } + await db.prepare("DELETE FROM workshops WHERE code=?").bind(code).run(); + await addAudit("管理员", "删除车间", `${code} ${workshop.name}`, "重要"); + } + } else if (action === "tag.add") { + const name = String(payload.name ?? "未命名标签").trim() || "未命名标签"; + const id = `E280-6102-${String(Math.floor(Math.random() * 9000) + 1000)}`; + await db.prepare("INSERT INTO rfid_tags(id,status,name,last_checked) VALUES(?,'未用',?,NULL)").bind(id, name).run(); + await addAudit("管理员", "新增 RFID", `${id} ${name}`); + } else if (action === "tag.status") { + const id = String(payload.id ?? ""); + const status = String(payload.status ?? "未用"); + await db.prepare("UPDATE rfid_tags SET status=?, last_checked=? WHERE id=?").bind(status, Date.now(), id).run(); + await addAudit("管理员", "RFID 状态变更", `${id} → ${status}`); + } else if (action === "alert.status") { + const id = String(payload.id ?? ""); + const status = String(payload.status ?? "处理中"); + await db.prepare("UPDATE alerts SET status=? WHERE id=?").bind(status, id).run(); + await addAudit("管理员", "处理告警", `${id} → ${status}`, "重要"); + } else if (action === "settings.save") { + const entries = Object.entries(payload); + for (const [key, value] of entries) { + await db + .prepare("INSERT INTO settings(key,value,updated_at) VALUES(?,?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at") + .bind(key, String(value), Date.now()) + .run(); + } + await addAudit("管理员", "更新系统设置", entries.map(([key]) => key).join("、"), "重要"); + } else if (action === "mes.sync") { + await db.prepare("UPDATE people SET sync_status='已同步', updated_at=? WHERE sync_status<>'已同步'").bind(Date.now()).run(); + await addAudit("管理员", "同步 MES 人员", "增量同步完成"); + } else { + return Response.json({ code: 1001, message: "未知操作" }, { status: 400 }); + } + + return Response.json({ code: 0, message: "操作成功", data: await loadConsoleData() }); +} diff --git a/management-console/app/api/v1/[...path]/route.ts b/management-console/app/api/v1/[...path]/route.ts new file mode 100644 index 0000000..4fa2c3b --- /dev/null +++ b/management-console/app/api/v1/[...path]/route.ts @@ -0,0 +1,121 @@ +import { addAudit, ensureDatabase } from "@/db/store"; + +export const dynamic = "force-dynamic"; + +type RouteContext = { params: Promise<{ path: string[] }> }; + +function ok(data: unknown, requestId = crypto.randomUUID()) { + return Response.json({ + code: 0, + message: "success", + requestId, + serverTime: Date.now(), + data, + }); +} + +function fail(code: number, message: string, status = 400) { + return Response.json( + { code, message, requestId: crypto.randomUUID(), serverTime: Date.now(), data: null }, + { status }, + ); +} + +export async function GET(request: Request, context: RouteContext) { + const db = await ensureDatabase(); + const path = (await context.params).path; + const url = new URL(request.url); + + if (path.length === 4 && path[0] === "devices" && path[2] === "people") { + const cursor = Number(url.searchParams.get("cursor") ?? 0); + const limit = Math.min(Number(url.searchParams.get("limit") ?? 200), 500); + const rows = await db + .prepare("SELECT id AS personId,name,workshop,role,active,face_status AS faceStatus,face_version AS faceVersion,updated_at AS updatedAt FROM people WHERE updated_at>? ORDER BY updated_at LIMIT ?") + .bind(cursor, limit) + .all(); + const nextCursor = rows.results.reduce((value, row) => Math.max(value, Number(row.updatedAt ?? 0)), cursor); + return ok({ items: rows.results, nextCursor, hasMore: rows.results.length === limit }); + } + + if (path.length === 4 && path[0] === "devices" && path[2] === "faces") { + const person = await db + .prepare("SELECT id AS personId,name,face_status AS faceStatus,face_version AS faceVersion,updated_at AS updatedAt FROM people WHERE id=? AND active=1") + .bind(path[3]) + .first(); + if (!person) return fail(4002, "PERSON_DELETED", 404); + return ok({ ...person, featureEncoding: "server-managed", feature: null }); + } + + if (path.length === 2 && path[0] === "deliveries") { + const row = await db.prepare("SELECT * FROM deliveries WHERE record_id=?").bind(path[1]).first(); + if (!row) return fail(3004, "DELIVERY_NOT_FOUND", 404); + return ok(row); + } + + return fail(1001, "接口不存在", 404); +} + +export async function POST(request: Request, context: RouteContext) { + const db = await ensureDatabase(); + const path = (await context.params).path; + const body = (await request.json().catch(() => ({}))) as Record; + + if (path.join("/") === "auth/tokens") { + const deviceId = String(body.deviceId ?? body.deviceNo ?? ""); + if (!deviceId) return fail(1001, "deviceId 必填"); + return ok({ accessToken: `local-${deviceId}-${Date.now()}`, tokenType: "Bearer", expiresIn: 28_800 }); + } + + if (path.length === 3 && path[0] === "devices" && path[2] === "heartbeat") { + const deviceId = path[1]; + const ip = String(body.ip ?? "0.0.0.0"); + await db + .prepare("INSERT INTO devices(id,type,ip,status,last_seen,heartbeat_seconds,fill_percent,category) VALUES(?,?,?,'在线',?,30,0,'终端') ON CONFLICT(id) DO UPDATE SET status='在线',last_seen=excluded.last_seen,ip=excluded.ip") + .bind(deviceId, String(body.deviceType ?? "回收终端"), ip, Date.now()) + .run(); + return ok({ accepted: true, nextHeartbeatIn: 30 }); + } + + if (path.length === 3 && path[0] === "devices" && path[2] === "status") { + const deviceId = path[1]; + const fillPercent = Number(body.fillPercent ?? body.fullRate ?? 0); + await db.prepare("UPDATE devices SET status=?,last_seen=?,fill_percent=? WHERE id=?").bind(String(body.status ?? "在线"), Date.now(), fillPercent, deviceId).run(); + return ok({ accepted: true }); + } + + if (path.length === 3 && path[0] === "devices" && path[2] === "alerts") { + const deviceId = path[1]; + const alerts = Array.isArray(body.alerts) ? body.alerts : [body]; + for (const item of alerts as Array>) { + await db + .prepare("INSERT OR IGNORE INTO alerts(id,created_at,type,severity,source,reference,description,status) VALUES(?,?,?,?,?,?,?,'待处理')") + .bind(String(item.alarmId ?? crypto.randomUUID()), Number(item.reportedAt ?? Date.now()), String(item.type ?? "设备告警"), String(item.severity ?? "警告"), "设备上报", deviceId, String(item.message ?? "设备异常")) + .run(); + } + return ok({ accepted: alerts.length }); + } + + if (path.length === 3 && path[0] === "devices" && path[2] === "deliveries") { + const deviceId = path[1]; + const recordId = String(body.recordId ?? ""); + if (!recordId) return fail(1001, "recordId 必填"); + const weight = Number(body.weight ?? body.afterWeight ?? 0); + await db + .prepare("INSERT OR IGNORE INTO deliveries(record_id,device_id,created_at,employee_name,employee_no,machine_no,production_batch,garbage_type,weight,payload,status) VALUES(?,?,?,?,?,?,?,?,?,?,?)") + .bind(recordId, deviceId, Number(body.createdAt ?? Date.now()), String(body.employeeName ?? body.person ?? ""), String(body.employeeNo ?? ""), String(body.machineNo ?? ""), String(body.productionBatch ?? ""), Number(body.garbageType ?? body.typeCode ?? 0), weight, JSON.stringify(body), "已接收") + .run(); + await db + .prepare("INSERT OR IGNORE INTO weighings(id,created_at,operator,machine,category,source,weight,event_id,status) VALUES(?,?,?,?,?,?,?,?,?)") + .bind(recordId, Number(body.createdAt ?? Date.now()), String(body.employeeName ?? body.person ?? "未知"), String(body.machineNo ?? "—"), `类型 ${String(body.garbageType ?? body.typeCode ?? 0)}`, deviceId, weight, String(body.eventId ?? recordId), "已确认") + .run(); + await addAudit(deviceId, "投放数据上报", `${recordId} · ${weight}kg`); + return ok({ accepted: true, duplicate: false }); + } + + if (path.length === 3 && path[0] === "devices" && path[2] === "sync-acks") { + await addAudit(path[1], "同步结果上报", JSON.stringify(body).slice(0, 500)); + return ok({ accepted: true }); + } + + return fail(1001, "接口不存在", 404); +} diff --git a/management-console/app/chatgpt-auth.ts b/management-console/app/chatgpt-auth.ts new file mode 100644 index 0000000..8d1fb35 --- /dev/null +++ b/management-console/app/chatgpt-auth.ts @@ -0,0 +1,86 @@ +import { headers } from "next/headers"; +import { redirect } from "next/navigation"; + +export type ChatGPTUser = { + displayName: string; + email: string; + fullName: string | null; +}; + +const USER_EMAIL_HEADER = "oai-authenticated-user-email"; +const USER_FULL_NAME_HEADER = "oai-authenticated-user-full-name"; +const USER_FULL_NAME_ENCODING_HEADER = + "oai-authenticated-user-full-name-encoding"; +const PERCENT_ENCODED_UTF8 = "percent-encoded-utf-8"; +const SIGN_IN_PATH = "/signin-with-chatgpt"; +const SIGN_OUT_PATH = "/signout-with-chatgpt"; +const CALLBACK_PATH = "/callback"; + +export async function getChatGPTUser(): Promise { + const requestHeaders = await headers(); + const email = requestHeaders.get(USER_EMAIL_HEADER); + if (!email) return null; + + const encodedFullName = requestHeaders.get(USER_FULL_NAME_HEADER); + const fullName = + encodedFullName && + requestHeaders.get(USER_FULL_NAME_ENCODING_HEADER) === PERCENT_ENCODED_UTF8 + ? safeDecodeURIComponent(encodedFullName) + : null; + + return { + displayName: fullName ?? email, + email, + fullName, + }; +} + +export async function requireChatGPTUser( + returnTo: string, +): Promise { + const user = await getChatGPTUser(); + if (user) return user; + + redirect(chatGPTSignInPath(returnTo)); +} + +export function chatGPTSignInPath(returnTo: string): string { + const safeReturnTo = safeRelativeReturnPath(returnTo); + return `${SIGN_IN_PATH}?return_to=${encodeURIComponent(safeReturnTo)}`; +} + +export function chatGPTSignOutPath(returnTo = "/"): string { + const safeReturnTo = safeRelativeReturnPath(returnTo); + return `${SIGN_OUT_PATH}?return_to=${encodeURIComponent(safeReturnTo)}`; +} + +function safeRelativeReturnPath(value: string): string { + if (!value.startsWith("/") || value.startsWith("//")) return "/"; + + let url: URL; + try { + url = new URL(value, "https://app.local"); + } catch { + return "/"; + } + if (url.origin !== "https://app.local") return "/"; + if (isReservedAuthPath(url.pathname)) return "/"; + + return `${url.pathname}${url.search}${url.hash}`; +} + +function isReservedAuthPath(pathname: string): boolean { + return ( + pathname === SIGN_IN_PATH || + pathname === SIGN_OUT_PATH || + pathname === CALLBACK_PATH + ); +} + +function safeDecodeURIComponent(value: string): string | null { + try { + return decodeURIComponent(value); + } catch { + return null; + } +} diff --git a/management-console/app/globals.css b/management-console/app/globals.css new file mode 100644 index 0000000..2dae7d0 --- /dev/null +++ b/management-console/app/globals.css @@ -0,0 +1,298 @@ +@import "tailwindcss"; + +:root { + --bg: #f3f5f8; + --panel: #ffffff; + --panel-soft: #f8fafc; + --ink: #172234; + --muted: #6b778a; + --line: #e2e7ee; + --nav: #142333; + --nav-soft: #1a2e43; + --blue: #1f72e5; + --cyan: #23a9c9; + --green: #21a675; + --orange: #f08a35; + --amber: #d99614; + --red: #dc4f4f; + --shadow: 0 14px 34px rgba(20, 35, 51, .07); +} + +* { box-sizing: border-box; } +html, body { margin: 0; min-height: 100%; background: var(--bg); color: var(--ink); } +body { + font-family: Inter, "PingFang SC", "Microsoft YaHei", system-ui, sans-serif; + -webkit-font-smoothing: antialiased; +} +button, input, select { font: inherit; } +button { cursor: pointer; } + +.loading-screen { + min-height: 100vh; display: grid; place-content: center; text-align: center; + background: radial-gradient(circle at 50% 30%, #fff, #edf2f8); +} +.loading-mark { + width: 64px; height: 64px; margin: 0 auto 18px; border-radius: 18px; + display: grid; place-items: center; color: white; font-size: 26px; font-weight: 800; + background: linear-gradient(135deg, var(--blue), var(--cyan)); box-shadow: 0 15px 30px rgba(31,114,229,.25); +} +.loading-screen h1 { font-size: 22px; margin: 0; } +.loading-screen p { color: var(--muted); margin: 10px 0 22px; } +.error-screen .loading-mark { background: var(--red); } + +.app-shell { min-height: 100vh; } +.sidebar { + position: fixed; inset: 0 auto 0 0; z-index: 20; width: 236px; display: flex; flex-direction: column; + color: #d9e4ef; background: + radial-gradient(circle at -10% 0, rgba(35,169,201,.18), transparent 28%), + linear-gradient(180deg, #142536 0%, #101d2a 100%); + border-right: 1px solid #20364b; +} +.brand { + min-height: 76px; padding: 17px 16px; display: flex; align-items: center; gap: 11px; + border-bottom: 1px solid rgba(255,255,255,.08); position: relative; +} +.brand-symbol { + width: 40px; height: 40px; display: grid; place-items: center; border-radius: 12px; + background: linear-gradient(135deg, #2686ff, #27b1c5); color: white; font-weight: 900; font-size: 17px; + box-shadow: 0 8px 20px rgba(31,114,229,.25); +} +.brand strong { display: block; font-size: 16px; letter-spacing: .08em; } +.brand small { display: block; margin-top: 3px; color: #71869a; font-size: 8px; letter-spacing: .18em; } +.version { position: absolute; top: 17px; right: 13px; color: #4fc0d8; font: 700 9px monospace; } +.sidebar nav { flex: 1; overflow: auto; padding: 10px 8px; } +.nav-group { margin-bottom: 8px; } +.nav-group p { padding: 8px 11px 5px; margin: 0; color: #647d92; font-size: 10px; letter-spacing: .16em; } +.nav-group button { + width: 100%; height: 39px; padding: 0 11px; display: flex; align-items: center; gap: 10px; + border: 0; border-radius: 8px; background: transparent; color: #9fb1c2; font-size: 13px; text-align: left; + transition: .18s ease; position: relative; +} +.nav-group button:hover { color: #fff; background: rgba(255,255,255,.05); } +.nav-group button.active { color: #fff; background: linear-gradient(90deg, rgba(31,114,229,.35), rgba(35,169,201,.13)); } +.nav-group button.active::before { content: ""; position: absolute; left: 0; width: 3px; height: 20px; border-radius: 4px; background: #47b9e9; } +.nav-icon { width: 19px; color: #5aa7d6; font-weight: 800; text-align: center; } +.nav-group button b { + margin-left: auto; min-width: 19px; height: 19px; display: grid; place-items: center; + color: white; background: var(--red); border-radius: 10px; font-size: 10px; +} +.sidebar-foot { padding: 13px 17px 18px; border-top: 1px solid rgba(255,255,255,.08); font-size: 11px; color: #8fa4b7; } +.sidebar-foot div { display: flex; align-items: center; gap: 7px; margin: 6px 0; } +.sidebar-foot strong { color: #d8e3ec; margin-left: auto; } +.sidebar-foot small { display: block; color: #586f83; margin-top: 8px; } +.online-dot, .offline-dot { width: 7px; height: 7px; border-radius: 50%; display: inline-block; background: #3bd29b; box-shadow: 0 0 0 4px rgba(59,210,155,.1); } +.offline-dot { background: var(--red); box-shadow: 0 0 0 4px rgba(220,79,79,.1); } + +.workspace { min-height: 100vh; margin-left: 236px; } +.topbar { + height: 76px; padding: 0 24px; display: flex; align-items: center; gap: 20px; + background: rgba(255,255,255,.9); backdrop-filter: blur(10px); border-bottom: 1px solid var(--line); + position: sticky; top: 0; z-index: 10; +} +.page-title { display: flex; align-items: baseline; gap: 11px; } +.page-title h1 { margin: 0; font-size: 20px; } +.page-title span { color: #9aa5b4; font-size: 10px; text-transform: uppercase; letter-spacing: .12em; } +.topbar-right { margin-left: auto; display: flex; align-items: center; gap: 14px; } +.connection { + display: flex; align-items: center; gap: 7px; padding: 6px 10px; color: var(--green); + background: #f2fbf7; border: 1px solid #cfefe2; border-radius: 8px; font-size: 11px; font-weight: 700; +} +.clock { padding-right: 4px; text-align: right; } +.clock strong { display: block; font: 700 13px ui-monospace, monospace; } +.clock span { display: block; margin-top: 2px; color: var(--muted); font-size: 9px; } +.alert-button { + width: 36px; height: 36px; border: 1px solid var(--line); border-radius: 9px; + background: white; color: var(--red); font-weight: 900; position: relative; +} +.alert-button b { position: absolute; top: -7px; right: -7px; min-width: 17px; height: 17px; line-height: 17px; border-radius: 9px; color: white; background: var(--red); font-size: 9px; } +.role-switch { padding: 3px; display: flex; background: #edf1f5; border-radius: 9px; } +.role-switch button { padding: 6px 10px; border: 0; border-radius: 7px; background: transparent; color: var(--muted); font-size: 11px; } +.role-switch button.active { color: #fff; background: var(--blue); box-shadow: 0 4px 10px rgba(31,114,229,.2); } +.content { max-width: 1500px; margin: 0 auto; padding: 22px 24px 40px; } +.page-stack { display: grid; gap: 16px; } + +.greeting { + padding: 18px 21px; display: flex; align-items: center; justify-content: space-between; + background: linear-gradient(125deg, #18334c, #164664 60%, #176079); color: white; + border-radius: 14px; box-shadow: var(--shadow); overflow: hidden; position: relative; +} +.greeting::after { content: ""; position: absolute; right: -60px; width: 280px; height: 280px; border: 54px solid rgba(255,255,255,.05); border-radius: 50%; } +.greeting span { color: #b9deee; font-size: 12px; } +.greeting h2 { margin: 4px 0 0; font-size: 18px; } +.greeting button { position: relative; z-index: 1; color: white; border-color: rgba(255,255,255,.2); background: rgba(255,255,255,.08); } +.kpi-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; } +.kpi { + min-height: 124px; padding: 18px; background: var(--panel); border: 1px solid var(--line); border-radius: 12px; + box-shadow: 0 8px 25px rgba(20,35,51,.04); position: relative; overflow: hidden; +} +.kpi::before { content: ""; position: absolute; inset: 0 auto 0 0; width: 4px; background: var(--blue); } +.kpi::after { content: ""; position: absolute; right: -20px; top: -20px; width: 84px; height: 84px; border-radius: 50%; background: rgba(31,114,229,.06); } +.kpi.orange::before { background: var(--orange); } .kpi.orange::after { background: rgba(240,138,53,.08); } +.kpi.green::before { background: var(--green); } .kpi.green::after { background: rgba(33,166,117,.08); } +.kpi.amber::before { background: var(--amber); } .kpi.amber::after { background: rgba(217,150,20,.08); } +.kpi > span { color: var(--muted); font-size: 12px; } +.kpi > strong { display: block; margin: 11px 0 7px; font-size: 28px; letter-spacing: -.03em; } +.kpi > small { color: #9aa5b4; font-size: 10px; } +.two-columns { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; } +.two-columns.wide-left { grid-template-columns: 1.45fr 1fr; } +.card { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; box-shadow: 0 8px 26px rgba(20,35,51,.04); overflow: hidden; } +.card-head { min-height: 61px; padding: 14px 17px; display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid var(--line); } +.card-head h3 { margin: 0; font-size: 14px; } +.card-head p { margin: 4px 0 0; color: var(--muted); font-size: 10px; } +.card-tools { display: flex; gap: 8px; } +.live-scale { min-height: 235px; padding: 34px 28px; display: grid; grid-template-columns: 1.2fr 1fr; align-items: center; background: radial-gradient(circle at 20% 50%, #eef8ff, transparent 48%); } +.live-scale > div > strong { color: var(--blue); font-size: 58px; letter-spacing: -.06em; } +.live-scale > div > span { margin-left: 8px; color: var(--muted); font-size: 16px; } +.live-scale p { display: flex; align-items: center; gap: 8px; color: var(--green); font-size: 11px; } +.live-scale dl { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; } +.live-scale dl div { padding: 10px; background: white; border: 1px solid var(--line); border-radius: 9px; } +.live-scale dt { color: var(--muted); font-size: 9px; } +.live-scale dd { margin: 5px 0 0; font-size: 12px; font-weight: 700; } +.device-list { padding: 8px 17px 13px; } +.device-list > div { min-height: 40px; display: grid; grid-template-columns: 12px 1fr auto; align-items: center; gap: 8px; border-bottom: 1px solid #eef1f4; } +.device-list > div:last-child { border: 0; } +.device-list span { display: flex; gap: 9px; color: var(--muted); font-size: 11px; } +.device-list span strong { width: 60px; color: var(--ink); } +.device-list small { color: #9aa5b4; font-size: 9px; } +.bar-chart { height: 190px; padding: 25px 25px 15px; display: flex; align-items: end; justify-content: space-around; gap: 13px; } +.bar-chart div { height: 100%; flex: 1; display: flex; flex-direction: column; justify-content: end; align-items: center; gap: 8px; } +.bar-chart i { width: min(24px, 65%); min-height: 10px; border-radius: 6px 6px 2px 2px; background: linear-gradient(180deg, #39b8d5, #247ae4); box-shadow: 0 5px 12px rgba(31,114,229,.15); } +.bar-chart span { color: var(--muted); font-size: 9px; } +.activity-list { padding: 7px 17px 13px; } +.activity-list > div { min-height: 38px; display: grid; grid-template-columns: 46px 1fr auto; align-items: center; gap: 8px; border-bottom: 1px solid #eef1f4; font-size: 11px; } +.activity-list time { color: #97a4b2; font: 600 10px ui-monospace, monospace; } +.activity-list span { color: var(--muted); } +.activity-list strong { font-size: 11px; } + +.toolbar { min-height: 58px; padding: 10px 15px; display: flex; align-items: center; gap: 10px; background: var(--panel-soft); border-bottom: 1px solid var(--line); } +.toolbar > :last-child { margin-left: auto; } +.tabs { display: flex; gap: 3px; } +.tabs button { height: 34px; padding: 0 13px; border: 1px solid transparent; border-radius: 8px; color: var(--muted); background: transparent; font-size: 11px; } +.tabs button.active { color: var(--blue); background: #eaf3ff; border-color: #d4e6ff; font-weight: 700; } +.search-box { width: min(310px, 35vw); height: 36px; padding: 0 11px; display: flex; align-items: center; gap: 8px; border: 1px solid var(--line); border-radius: 9px; background: white; color: #99a5b3; } +.search-box input { width: 100%; border: 0; outline: 0; background: transparent; color: var(--ink); font-size: 11px; } +.primary-button, .ghost-button { + min-height: 34px; padding: 0 14px; border-radius: 8px; font-size: 11px; font-weight: 700; transition: .16s; +} +.primary-button { color: white; border: 1px solid var(--blue); background: var(--blue); box-shadow: 0 5px 12px rgba(31,114,229,.15); } +.primary-button:hover { background: #1767d2; } +.primary-button:disabled { opacity: .55; cursor: wait; } +.ghost-button { color: #476079; border: 1px solid var(--line); background: white; } +.ghost-button:hover { border-color: #b7c4d1; color: var(--blue); } +.table-wrap { overflow-x: auto; } +table { width: 100%; min-width: 850px; border-collapse: collapse; } +th { padding: 11px 14px; color: #8794a3; background: #fbfcfd; border-bottom: 1px solid var(--line); text-align: left; font-size: 10px; font-weight: 700; white-space: nowrap; } +td { padding: 12px 14px; border-bottom: 1px solid #edf0f4; color: #4c5a6c; font-size: 11px; white-space: nowrap; } +tbody tr:hover { background: #f9fbfd; } +td strong { color: var(--ink); } +td select { max-width: 150px; padding: 5px 8px; border: 1px solid var(--line); border-radius: 6px; background: white; color: var(--ink); font-size: 10px; } +.empty { height: 180px; text-align: center; color: #9aa5b4; } +.status { display: inline-flex; align-items: center; gap: 4px; padding: 3px 7px; border: 1px solid; border-radius: 12px; font-size: 9px; font-weight: 700; } +.status.ok { color: #17835c; border-color: #bce7d5; background: #effaf5; } +.status.warn { color: #9a6d0d; border-color: #f0d89d; background: #fff9e8; } +.status.bad { color: #b73939; border-color: #f0c3c3; background: #fff3f3; } +.status.muted { color: #6e7c8d; border-color: #dce2e8; background: #f6f8fa; } +.row-actions { display: flex; gap: 6px; } +.row-actions button { padding: 4px 8px; border: 1px solid #d9e1e9; border-radius: 6px; color: #52708f; background: white; font-size: 9px; } +.row-actions button:hover { color: var(--blue); border-color: #a9c8ed; } +.statistics-grid { padding: 28px; display: grid; grid-template-columns: repeat(4,1fr); gap: 15px; } +.statistics-grid > div { padding: 18px; border: 1px solid var(--line); border-radius: 10px; background: #fbfcfe; overflow: hidden; } +.statistics-grid span { color: var(--muted); font-size: 11px; } +.statistics-grid strong { display: block; margin: 9px 0 14px; font-size: 22px; } +.statistics-grid i { display: block; height: 5px; border-radius: 4px; background: linear-gradient(90deg, var(--blue), var(--cyan)); } + +.device-grid { padding: 15px; display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; } +.device-grid article { min-height: 160px; padding: 15px; border: 1px solid var(--line); border-radius: 10px; background: linear-gradient(145deg, #fff, #f7fafc); } +.device-grid article > div:first-child { display: flex; align-items: center; justify-content: space-between; } +.device-grid article h4 { margin: 16px 0 5px; font-size: 14px; } +.device-grid article p, .device-grid article small, .device-grid article footer { color: var(--muted); font-size: 10px; } +.device-grid article footer { margin-top: 13px; padding-top: 10px; border-top: 1px solid var(--line); } +.meter { height: 6px; margin: 16px 0 7px; overflow: hidden; background: #e7ebef; border-radius: 5px; } +.meter i { display: block; height: 100%; border-radius: inherit; background: linear-gradient(90deg, var(--green), var(--amber)); } +.alert-list { padding: 9px 15px 15px; display: grid; gap: 9px; } +.alert-list article { min-height: 86px; padding: 13px; display: grid; grid-template-columns: 34px 1fr auto; align-items: center; gap: 13px; border: 1px solid #eadfbf; border-left: 3px solid var(--amber); border-radius: 9px; background: #fffdf7; } +.alert-list article.critical { border-color: #ecc7c7; border-left-color: var(--red); background: #fff9f9; } +.alert-sign { width: 32px; height: 32px; display: grid; place-items: center; border-radius: 9px; color: white; background: var(--amber); font-weight: 900; } +.critical .alert-sign { background: var(--red); } +.alert-title { display: flex; align-items: center; gap: 8px; } +.alert-title h3 { margin: 0; font-size: 13px; } +.alert-list p { margin: 5px 0; color: #596879; font-size: 11px; } +.alert-list small { color: #97a3af; font-size: 9px; } +.sub-toolbar { min-height: 50px; padding: 8px 15px; display: flex; align-items: center; justify-content: space-between; color: var(--muted); font-size: 10px; border-bottom: 1px solid var(--line); } +.settings-form { max-width: 720px; padding: 24px; display: grid; gap: 14px; } +.settings-form > label, .modal-body label { display: grid; grid-template-columns: 110px 1fr auto; align-items: center; gap: 10px; } +.settings-form label span, .modal-body label span { color: var(--muted); font-size: 11px; } +.settings-form input, .settings-form select, .modal-body input { + height: 38px; padding: 0 11px; border: 1px solid var(--line); border-radius: 8px; outline: 0; color: var(--ink); background: white; font-size: 11px; +} +.settings-form input:focus, .modal-body input:focus { border-color: #8bb8ee; box-shadow: 0 0 0 3px rgba(31,114,229,.08); } +.settings-form small { color: var(--muted); font-size: 10px; } +.form-actions { padding-left: 120px; display: flex; align-items: center; gap: 9px; } + +.simulator-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; } +.sim-card .card-head { background: #fbfcfd; } +.terminal { + width: 100%; min-height: 520px; padding: 48px 38px; display: flex; flex-direction: column; align-items: center; justify-content: center; + border: 18px solid #1e2b38; background: radial-gradient(circle at 50% 42%, #f9fcff, #eaf1f7); color: var(--ink); position: relative; +} +.terminal-camera { position: absolute; top: -12px; width: 7px; height: 7px; border-radius: 50%; background: #090e13; box-shadow: 0 0 0 2px #354758; } +.terminal > small { position: absolute; top: 14px; left: 17px; color: #7d8b99; font: 600 9px ui-monospace, monospace; } +.terminal-icon { width: 100px; height: 100px; display: grid; place-items: center; border: 2px solid var(--orange); border-radius: 50%; color: var(--orange); font-size: 38px; box-shadow: 0 0 0 12px rgba(240,138,53,.08); } +.terminal.blue .terminal-icon { color: var(--blue); border-color: var(--blue); box-shadow: 0 0 0 12px rgba(31,114,229,.08); } +.terminal h3 { margin: 33px 0 10px; font-size: 24px; } +.terminal p { color: var(--muted); font-size: 12px; } +.terminal > strong { margin-top: 25px; padding: 11px 18px; border-radius: 8px; color: white; background: var(--orange); font-size: 11px; } +.terminal.blue > strong { background: var(--blue); } +.step-dots { position: absolute; bottom: 20px; display: flex; gap: 7px; } +.step-dots i { width: 7px; height: 7px; border-radius: 50%; background: #c5ced6; } +.step-dots i.active { width: 22px; border-radius: 5px; background: var(--orange); } +.terminal.blue .step-dots i.active { background: var(--blue); } + +.modal-backdrop { position: fixed; inset: 0; z-index: 50; display: grid; place-items: center; background: rgba(12,24,37,.42); backdrop-filter: blur(4px); } +.modal { width: min(500px, calc(100vw - 30px)); border-radius: 14px; background: white; box-shadow: 0 28px 70px rgba(12,24,37,.25); overflow: hidden; } +.modal header { height: 60px; padding: 0 18px; display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid var(--line); } +.modal header h2 { margin: 0; font-size: 16px; } +.modal header button { width: 32px; height: 32px; border: 0; border-radius: 8px; color: var(--muted); background: #f3f5f7; font-size: 20px; } +.modal-body { padding: 22px; display: grid; gap: 13px; } +.modal footer { padding: 13px 18px; display: flex; justify-content: end; gap: 8px; border-top: 1px solid var(--line); background: #fbfcfd; } +.toast { position: fixed; right: 24px; bottom: 24px; z-index: 70; padding: 12px 16px; color: white; background: #17334b; border-radius: 9px; box-shadow: 0 13px 30px rgba(12,24,37,.25); font-size: 11px; animation: toast-in .25s ease; } +@keyframes toast-in { from { opacity: 0; transform: translateY(10px); } } + +@media (max-width: 1150px) { + .sidebar { width: 205px; } + .workspace { margin-left: 205px; } + .kpi-grid { grid-template-columns: 1fr 1fr; } + .device-grid { grid-template-columns: 1fr 1fr; } + .connection, .clock { display: none; } +} + +@media (max-width: 820px) { + .sidebar { position: static; width: 100%; height: auto; } + .sidebar nav { display: flex; overflow-x: auto; } + .nav-group { display: contents; } + .nav-group p, .sidebar-foot { display: none; } + .nav-group button { min-width: 110px; justify-content: center; } + .workspace { margin-left: 0; } + .topbar { height: auto; min-height: 72px; align-items: flex-start; padding: 12px 14px; } + .topbar-right { flex-wrap: wrap; justify-content: end; } + .role-switch { order: 3; width: 100%; justify-content: end; } + .content { padding: 14px; } + .two-columns, .two-columns.wide-left, .simulator-grid { grid-template-columns: 1fr; } + .kpi-grid { grid-template-columns: 1fr 1fr; } + .device-grid, .statistics-grid { grid-template-columns: 1fr; } + .search-box { width: 100%; } + .toolbar { align-items: stretch; flex-direction: column; } + .toolbar > :last-child { margin-left: 0; } + .tabs { overflow-x: auto; } + .live-scale { grid-template-columns: 1fr; gap: 18px; } +} + +@media (max-width: 520px) { + .brand { min-height: 64px; } + .page-title span, .alert-button { display: none; } + .kpi-grid { grid-template-columns: 1fr; } + .greeting { align-items: flex-start; gap: 12px; flex-direction: column; } + .terminal { min-height: 430px; border-width: 11px; } + .modal-body label, .settings-form > label { grid-template-columns: 1fr; } + .form-actions { padding-left: 0; } +} diff --git a/management-console/app/layout.tsx b/management-console/app/layout.tsx new file mode 100644 index 0000000..58f5fe3 --- /dev/null +++ b/management-console/app/layout.tsx @@ -0,0 +1,44 @@ +import type { Metadata } from "next"; +import { headers } from "next/headers"; +import "./globals.css"; + +export async function generateMetadata(): Promise { + const requestHeaders = await headers(); + const host = requestHeaders.get("x-forwarded-host") ?? requestHeaders.get("host") ?? "localhost:3000"; + const protocol = requestHeaders.get("x-forwarded-proto") ?? (host.startsWith("localhost") ? "http" : "https"); + const baseUrl = new URL(`${protocol}://${host}`); + + return { + metadataBase: baseUrl, + title: "智能回收运营平台", + description: "面向内网部署的称重、设备、人员、人脸与告警管理后台", + icons: { + icon: "/favicon.svg", + shortcut: "/favicon.svg", + }, + openGraph: { + title: "智能回收运营平台", + description: "称重、设备、人员与告警的一体化内网管理平台", + type: "website", + images: [{ url: new URL("/og.png", baseUrl).toString(), width: 1672, height: 941 }], + }, + twitter: { + card: "summary_large_image", + title: "智能回收运营平台", + description: "称重、设备、人员与告警的一体化内网管理平台", + images: [new URL("/og.png", baseUrl).toString()], + }, + }; +} + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + {children} + + ); +} diff --git a/management-console/app/page.tsx b/management-console/app/page.tsx new file mode 100644 index 0000000..ebed98d --- /dev/null +++ b/management-console/app/page.tsx @@ -0,0 +1,5 @@ +import { AdminConsole } from "./AdminConsole"; + +export default function Home() { + return ; +} diff --git a/management-console/db/index.ts b/management-console/db/index.ts new file mode 100644 index 0000000..19b3799 --- /dev/null +++ b/management-console/db/index.ts @@ -0,0 +1,13 @@ +import { env } from "cloudflare:workers"; +import { drizzle } from "drizzle-orm/d1"; +import * as schema from "./schema"; + +export function getDb() { + if (!env.DB) { + throw new Error( + "Cloudflare D1 binding `DB` is unavailable. Set the `d1` field in .openai/hosting.json to `DB` or let your control plane inject the real binding values before using the database." + ); + } + + return drizzle(env.DB, { schema }); +} diff --git a/management-console/db/schema.ts b/management-console/db/schema.ts new file mode 100644 index 0000000..ba0e0bb --- /dev/null +++ b/management-console/db/schema.ts @@ -0,0 +1,127 @@ +import { integer, real, sqliteTable, text } from "drizzle-orm/sqlite-core"; + +export const people = sqliteTable("people", { + id: text("id").primaryKey(), + name: text("name").notNull(), + workshop: text("workshop").notNull(), + role: text("role").notNull(), + faceStatus: text("face_status").notNull(), + faceVersion: integer("face_version").notNull().default(1), + syncStatus: text("sync_status").notNull(), + active: integer("active", { mode: "boolean" }).notNull().default(true), + updatedAt: integer("updated_at").notNull(), +}); + +export const workshops = sqliteTable("workshops", { + code: text("code").primaryKey(), + name: text("name").notNull().unique(), + location: text("location").notNull(), + note: text("note").notNull(), +}); + +export const devices = sqliteTable("devices", { + id: text("id").primaryKey(), + type: text("type").notNull(), + ip: text("ip").notNull(), + status: text("status").notNull(), + lastSeen: integer("last_seen").notNull(), + heartbeatSeconds: integer("heartbeat_seconds").notNull(), + fillPercent: integer("fill_percent").notNull().default(0), + category: text("category").notNull().default(""), +}); + +export const weighings = sqliteTable("weighings", { + id: text("id").primaryKey(), + createdAt: integer("created_at").notNull(), + operator: text("operator").notNull(), + machine: text("machine").notNull(), + category: text("category").notNull(), + source: text("source").notNull(), + weight: real("weight").notNull(), + eventId: text("event_id").notNull(), + status: text("status").notNull(), +}); + +export const events = sqliteTable("events", { + id: text("id").primaryKey(), + operator: text("operator").notNull(), + enteredAt: integer("entered_at").notNull(), + leftAt: integer("left_at").notNull(), + atomicCount: integer("atomic_count").notNull(), + scaleTotal: real("scale_total").notNull(), + verdict: text("verdict").notNull(), +}); + +export const reconciliations = sqliteTable("reconciliations", { + id: text("id").primaryKey(), + eventId: text("event_id").notNull(), + category: text("category").notNull(), + smallScale: real("small_scale").notNull(), + floorScale: real("floor_scale").notNull(), + difference: real("difference").notNull(), + differenceRate: real("difference_rate").notNull(), + verdict: text("verdict").notNull(), + storageStatus: text("storage_status").notNull(), +}); + +export const traces = sqliteTable("traces", { + id: text("id").primaryKey(), + createdAt: integer("created_at").notNull(), + workshop: text("workshop").notNull(), + taskId: text("task_id").notNull(), + rfid: text("rfid").notNull(), + batch: text("batch").notNull(), + tare: real("tare").notNull(), + gross: real("gross").notNull(), + net: real("net").notNull(), + machine: text("machine").notNull(), + operator: text("operator").notNull(), + mesStatus: text("mes_status").notNull(), +}); + +export const rfidTags = sqliteTable("rfid_tags", { + id: text("id").primaryKey(), + status: text("status").notNull(), + name: text("name").notNull(), + lastChecked: integer("last_checked"), +}); + +export const alerts = sqliteTable("alerts", { + id: text("id").primaryKey(), + createdAt: integer("created_at").notNull(), + type: text("type").notNull(), + severity: text("severity").notNull(), + source: text("source").notNull(), + reference: text("reference").notNull(), + description: text("description").notNull(), + status: text("status").notNull(), +}); + +export const settings = sqliteTable("settings", { + key: text("key").primaryKey(), + value: text("value").notNull(), + updatedAt: integer("updated_at").notNull(), +}); + +export const auditLogs = sqliteTable("audit_logs", { + id: text("id").primaryKey(), + createdAt: integer("created_at").notNull(), + actor: text("actor").notNull(), + action: text("action").notNull(), + details: text("details").notNull(), + level: text("level").notNull(), +}); + +export const deliveries = sqliteTable("deliveries", { + recordId: text("record_id").primaryKey(), + deviceId: text("device_id").notNull(), + createdAt: integer("created_at").notNull(), + employeeName: text("employee_name").notNull(), + employeeNo: text("employee_no").notNull(), + machineNo: text("machine_no").notNull(), + productionBatch: text("production_batch").notNull(), + garbageType: integer("garbage_type").notNull(), + weight: real("weight").notNull(), + payload: text("payload").notNull(), + status: text("status").notNull(), +}); diff --git a/management-console/db/store.ts b/management-console/db/store.ts new file mode 100644 index 0000000..662209d --- /dev/null +++ b/management-console/db/store.ts @@ -0,0 +1,184 @@ +import { env } from "cloudflare:workers"; + +const schemaStatements = [ + `CREATE TABLE IF NOT EXISTS people ( + id TEXT PRIMARY KEY, name TEXT NOT NULL, workshop TEXT NOT NULL, role TEXT NOT NULL, + face_status TEXT NOT NULL, face_version INTEGER NOT NULL DEFAULT 1, + sync_status TEXT NOT NULL, active INTEGER NOT NULL DEFAULT 1, updated_at INTEGER NOT NULL + )`, + `CREATE TABLE IF NOT EXISTS workshops ( + code TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, location TEXT NOT NULL, note TEXT NOT NULL + )`, + `CREATE TABLE IF NOT EXISTS devices ( + id TEXT PRIMARY KEY, type TEXT NOT NULL, ip TEXT NOT NULL, status TEXT NOT NULL, + last_seen INTEGER NOT NULL, heartbeat_seconds INTEGER NOT NULL, + fill_percent INTEGER NOT NULL DEFAULT 0, category TEXT NOT NULL DEFAULT '' + )`, + `CREATE TABLE IF NOT EXISTS weighings ( + id TEXT PRIMARY KEY, created_at INTEGER NOT NULL, operator TEXT NOT NULL, + machine TEXT NOT NULL, category TEXT NOT NULL, source TEXT NOT NULL, + weight REAL NOT NULL, event_id TEXT NOT NULL, status TEXT NOT NULL + )`, + `CREATE TABLE IF NOT EXISTS events ( + id TEXT PRIMARY KEY, operator TEXT NOT NULL, entered_at INTEGER NOT NULL, + left_at INTEGER NOT NULL, atomic_count INTEGER NOT NULL, + scale_total REAL NOT NULL, verdict TEXT NOT NULL + )`, + `CREATE TABLE IF NOT EXISTS reconciliations ( + id TEXT PRIMARY KEY, event_id TEXT NOT NULL, category TEXT NOT NULL, + small_scale REAL NOT NULL, floor_scale REAL NOT NULL, difference REAL NOT NULL, + difference_rate REAL NOT NULL, verdict TEXT NOT NULL, storage_status TEXT NOT NULL + )`, + `CREATE TABLE IF NOT EXISTS traces ( + id TEXT PRIMARY KEY, created_at INTEGER NOT NULL, workshop TEXT NOT NULL, + task_id TEXT NOT NULL, rfid TEXT NOT NULL, batch TEXT NOT NULL, + tare REAL NOT NULL, gross REAL NOT NULL, net REAL NOT NULL, + machine TEXT NOT NULL, operator TEXT NOT NULL, mes_status TEXT NOT NULL + )`, + `CREATE TABLE IF NOT EXISTS rfid_tags ( + id TEXT PRIMARY KEY, status TEXT NOT NULL, name TEXT NOT NULL, last_checked INTEGER + )`, + `CREATE TABLE IF NOT EXISTS alerts ( + id TEXT PRIMARY KEY, created_at INTEGER NOT NULL, type TEXT NOT NULL, + severity TEXT NOT NULL, source TEXT NOT NULL, reference TEXT NOT NULL, + description TEXT NOT NULL, status TEXT NOT NULL + )`, + `CREATE TABLE IF NOT EXISTS settings ( + key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at INTEGER NOT NULL + )`, + `CREATE TABLE IF NOT EXISTS audit_logs ( + id TEXT PRIMARY KEY, created_at INTEGER NOT NULL, actor TEXT NOT NULL, + action TEXT NOT NULL, details TEXT NOT NULL, level TEXT NOT NULL + )`, + `CREATE TABLE IF NOT EXISTS deliveries ( + record_id TEXT PRIMARY KEY, device_id TEXT NOT NULL, created_at INTEGER NOT NULL, + employee_name TEXT NOT NULL, employee_no TEXT NOT NULL, machine_no TEXT NOT NULL, + production_batch TEXT NOT NULL, garbage_type INTEGER NOT NULL, weight REAL NOT NULL, + payload TEXT NOT NULL, status TEXT NOT NULL + )`, + `CREATE INDEX IF NOT EXISTS weighings_created_at_idx ON weighings(created_at)`, + `CREATE INDEX IF NOT EXISTS alerts_status_idx ON alerts(status)`, + `CREATE INDEX IF NOT EXISTS people_updated_at_idx ON people(updated_at)`, +]; + +const now = Date.now(); +const minute = 60_000; + +const seedStatements: Array<[string, unknown[]]> = [ + ["INSERT OR IGNORE INTO workshops(code,name,location,note) VALUES(?,?,?,?)", ["WS-01", "一楼新能源车间", "1F · 东侧", "废料回收主区域"]], + ["INSERT OR IGNORE INTO workshops(code,name,location,note) VALUES(?,?,?,?)", ["WS-02", "二楼导体车间", "2F · 南侧", "导体称重与追溯"]], + ["INSERT OR IGNORE INTO workshops(code,name,location,note) VALUES(?,?,?,?)", ["WS-03", "仓储中心", "1F · 西侧", "地磅与成品暂存"]], + ["INSERT OR IGNORE INTO people(id,name,workshop,role,face_status,face_version,sync_status,active,updated_at) VALUES(?,?,?,?,?,?,?,?,?)", ["P1001", "张三", "一楼新能源车间", "仓管员", "已录入", 3, "已同步", 1, now - 3 * minute]], + ["INSERT OR IGNORE INTO people(id,name,workshop,role,face_status,face_version,sync_status,active,updated_at) VALUES(?,?,?,?,?,?,?,?,?)", ["P1002", "李四", "二楼导体车间", "产线工人", "已录入", 2, "已同步", 1, now - 8 * minute]], + ["INSERT OR IGNORE INTO people(id,name,workshop,role,face_status,face_version,sync_status,active,updated_at) VALUES(?,?,?,?,?,?,?,?,?)", ["P1003", "赵六", "一楼新能源车间", "仓管员", "需更新", 4, "同步失败", 1, now - 12 * minute]], + ["INSERT OR IGNORE INTO people(id,name,workshop,role,face_status,face_version,sync_status,active,updated_at) VALUES(?,?,?,?,?,?,?,?,?)", ["P1004", "王工", "仓储中心", "管理员", "已录入", 1, "已同步", 1, now - 20 * minute]], + ["INSERT OR IGNORE INTO devices(id,type,ip,status,last_seen,heartbeat_seconds,fill_percent,category) VALUES(?,?,?,?,?,?,?,?)", ["WKS-01", "入库触摸屏", "192.168.10.11", "在线", now - 2_000, 30, 0, "终端"]], + ["INSERT OR IGNORE INTO devices(id,type,ip,status,last_seen,heartbeat_seconds,fill_percent,category) VALUES(?,?,?,?,?,?,?,?)", ["SB-01", "小秤台", "192.168.10.12", "在线", now - 3_000, 30, 0, "称重"]], + ["INSERT OR IGNORE INTO devices(id,type,ip,status,last_seen,heartbeat_seconds,fill_percent,category) VALUES(?,?,?,?,?,?,?,?)", ["IB-01", "智能回收箱 A", "192.168.10.13", "在线", now - 5_000, 30, 72, "废铜"]], + ["INSERT OR IGNORE INTO devices(id,type,ip,status,last_seen,heartbeat_seconds,fill_percent,category) VALUES(?,?,?,?,?,?,?,?)", ["IB-02", "智能回收箱 B", "192.168.10.14", "在线", now - 4_000, 30, 46, "废电线"]], + ["INSERT OR IGNORE INTO devices(id,type,ip,status,last_seen,heartbeat_seconds,fill_percent,category) VALUES(?,?,?,?,?,?,?,?)", ["FS-01", "3 吨地磅", "192.168.10.15", "在线", now - 1_000, 30, 0, "称重"]], + ["INSERT OR IGNORE INTO devices(id,type,ip,status,last_seen,heartbeat_seconds,fill_percent,category) VALUES(?,?,?,?,?,?,?,?)", ["RFID-01", "RFID 读写器", "192.168.10.16", "在线", now - 6_000, 30, 0, "追溯"]], + ["INSERT OR IGNORE INTO weighings(id,created_at,operator,machine,category,source,weight,event_id,status) VALUES(?,?,?,?,?,?,?,?,?)", ["W-0001", now - 26 * minute, "张三", "MC-101", "废铜", "SB-01", 12.48, "EV-0728-001", "已确认"]], + ["INSERT OR IGNORE INTO weighings(id,created_at,operator,machine,category,source,weight,event_id,status) VALUES(?,?,?,?,?,?,?,?,?)", ["W-0002", now - 24 * minute, "张三", "MC-101", "废铜", "SB-01", 9.82, "EV-0728-001", "已确认"]], + ["INSERT OR IGNORE INTO weighings(id,created_at,operator,machine,category,source,weight,event_id,status) VALUES(?,?,?,?,?,?,?,?,?)", ["W-0003", now - 18 * minute, "赵六", "MC-108", "废电线", "IB-02", 14.50, "EV-0728-002", "偏差超阈"]], + ["INSERT OR IGNORE INTO weighings(id,created_at,operator,machine,category,source,weight,event_id,status) VALUES(?,?,?,?,?,?,?,?,?)", ["W-0004", now - 9 * minute, "李四", "MC-105", "废锡", "IB-01", 19.50, "EV-0728-003", "已确认"]], + ["INSERT OR IGNORE INTO events(id,operator,entered_at,left_at,atomic_count,scale_total,verdict) VALUES(?,?,?,?,?,?,?)", ["EV-0728-001", "张三", now - 30 * minute, now - 22 * minute, 2, 22.30, "正常"]], + ["INSERT OR IGNORE INTO events(id,operator,entered_at,left_at,atomic_count,scale_total,verdict) VALUES(?,?,?,?,?,?,?)", ["EV-0728-002", "赵六", now - 21 * minute, now - 14 * minute, 1, 14.50, "超标"]], + ["INSERT OR IGNORE INTO events(id,operator,entered_at,left_at,atomic_count,scale_total,verdict) VALUES(?,?,?,?,?,?,?)", ["EV-0728-003", "李四", now - 12 * minute, now - 6 * minute, 1, 19.50, "警告"]], + ["INSERT OR IGNORE INTO reconciliations(id,event_id,category,small_scale,floor_scale,difference,difference_rate,verdict,storage_status) VALUES(?,?,?,?,?,?,?,?,?)", ["RC-001", "EV-0728-001", "废铜", 22.30, 22.60, 0.30, 1.35, "正常", "已自动入库"]], + ["INSERT OR IGNORE INTO reconciliations(id,event_id,category,small_scale,floor_scale,difference,difference_rate,verdict,storage_status) VALUES(?,?,?,?,?,?,?,?,?)", ["RC-002", "EV-0728-002", "废电线", 14.50, 15.40, 0.90, 6.21, "超标", "已入库 · 异常"]], + ["INSERT OR IGNORE INTO reconciliations(id,event_id,category,small_scale,floor_scale,difference,difference_rate,verdict,storage_status) VALUES(?,?,?,?,?,?,?,?,?)", ["RC-003", "EV-0728-003", "废锡", 19.50, 20.10, 0.60, 3.08, "警告", "已入库 · 预警"]], + ["INSERT OR IGNORE INTO traces(id,created_at,workshop,task_id,rfid,batch,tare,gross,net,machine,operator,mes_status) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)", ["TR-001", now - 48 * minute, "二楼导体车间", "WT-260728-03", "E280-6102-0345", "B2607-018", 152.30, 268.10, 115.80, "MC-201", "李四", "已推送"]], + ["INSERT OR IGNORE INTO traces(id,created_at,workshop,task_id,rfid,batch,tare,gross,net,machine,operator,mes_status) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)", ["TR-002", now - 90 * minute, "二楼导体车间", "WT-260728-02", "E280-6102-0288", "B2607-017", 148.20, 252.70, 104.50, "MC-203", "周七", "待推送"]], + ["INSERT OR IGNORE INTO rfid_tags(id,status,name,last_checked) VALUES(?,?,?,?)", ["E280-6102-0345", "在用", "8# 导体轴标签", now - 48 * minute]], + ["INSERT OR IGNORE INTO rfid_tags(id,status,name,last_checked) VALUES(?,?,?,?)", ["E280-6102-0288", "在用", "7# 导体轴标签", now - 90 * minute]], + ["INSERT OR IGNORE INTO rfid_tags(id,status,name,last_checked) VALUES(?,?,?,?)", ["E280-6102-0411", "未用", "备用标签", null]], + ["INSERT OR IGNORE INTO alerts(id,created_at,type,severity,source,reference,description,status) VALUES(?,?,?,?,?,?,?,?)", ["AL-001", now - 16 * minute, "满溢预警", "警告", "废料库", "IB-01", "满溢率达到 72%,建议安排清运", "待处理"]], + ["INSERT OR IGNORE INTO alerts(id,created_at,type,severity,source,reference,description,status) VALUES(?,?,?,?,?,?,?,?)", ["AL-002", now - 42 * minute, "双秤偏差", "严重", "地磅核对", "EV-0728-002", "地磅增量与台秤汇总偏差 6.21%", "处理中"]], + ["INSERT OR IGNORE INTO settings(key,value,updated_at) VALUES(?,?,?)", ["mes_url", "http://mes.local/api/v2", now]], + ["INSERT OR IGNORE INTO settings(key,value,updated_at) VALUES(?,?,?)", ["mes_timeout", "10", now]], + ["INSERT OR IGNORE INTO settings(key,value,updated_at) VALUES(?,?,?)", ["deviation_warn", "2", now]], + ["INSERT OR IGNORE INTO settings(key,value,updated_at) VALUES(?,?,?)", ["deviation_critical", "5", now]], + ["INSERT OR IGNORE INTO settings(key,value,updated_at) VALUES(?,?,?)", ["fill_warn", "70", now]], + ["INSERT OR IGNORE INTO settings(key,value,updated_at) VALUES(?,?,?)", ["fill_critical", "90", now]], + ["INSERT OR IGNORE INTO audit_logs(id,created_at,actor,action,details,level) VALUES(?,?,?,?,?,?)", ["LOG-001", now - 15 * minute, "系统", "设备心跳", "WKS-01 状态恢复为在线", "信息"]], + ["INSERT OR IGNORE INTO audit_logs(id,created_at,actor,action,details,level) VALUES(?,?,?,?,?,?)", ["LOG-002", now - 38 * minute, "王工", "处理告警", "AL-002 标记为处理中", "重要"]], +]; + +export function getD1(): D1Database { + if (!env.DB) { + throw new Error("D1 数据库未绑定,请将 .openai/hosting.json 的 d1 设置为 DB。"); + } + return env.DB; +} + +export async function ensureDatabase() { + const db = getD1(); + await db.batch(schemaStatements.map((statement) => db.prepare(statement))); + await db.batch( + seedStatements.map(([statement, bindings]) => + db.prepare(statement).bind(...bindings), + ), + ); + return db; +} + +export async function addAudit( + actor: string, + action: string, + details: string, + level = "信息", +) { + const db = getD1(); + await db + .prepare( + "INSERT INTO audit_logs(id,created_at,actor,action,details,level) VALUES(?,?,?,?,?,?)", + ) + .bind(crypto.randomUUID(), Date.now(), actor, action, details, level) + .run(); +} + +export async function loadConsoleData() { + const db = await ensureDatabase(); + const [ + peopleResult, + workshopResult, + deviceResult, + weighingResult, + eventResult, + reconciliationResult, + traceResult, + tagResult, + alertResult, + settingResult, + logResult, + ] = await Promise.all([ + db.prepare("SELECT * FROM people ORDER BY active DESC, name").all(), + db.prepare("SELECT * FROM workshops ORDER BY code").all(), + db.prepare("SELECT * FROM devices ORDER BY id").all(), + db.prepare("SELECT * FROM weighings ORDER BY created_at DESC LIMIT 200").all(), + db.prepare("SELECT * FROM events ORDER BY entered_at DESC LIMIT 100").all(), + db.prepare("SELECT * FROM reconciliations ORDER BY event_id DESC LIMIT 100").all(), + db.prepare("SELECT * FROM traces ORDER BY created_at DESC LIMIT 200").all(), + db.prepare("SELECT * FROM rfid_tags ORDER BY status, id").all(), + db.prepare("SELECT * FROM alerts ORDER BY created_at DESC LIMIT 200").all(), + db.prepare("SELECT * FROM settings ORDER BY key").all(), + db.prepare("SELECT * FROM audit_logs ORDER BY created_at DESC LIMIT 200").all(), + ]); + return { + people: peopleResult.results, + workshops: workshopResult.results, + devices: deviceResult.results, + weighings: weighingResult.results, + events: eventResult.results, + reconciliations: reconciliationResult.results, + traces: traceResult.results, + tags: tagResult.results, + alerts: alertResult.results, + settings: Object.fromEntries( + settingResult.results.map((row) => [String(row.key), String(row.value)]), + ), + logs: logResult.results, + serverTime: Date.now(), + }; +} diff --git a/management-console/docker-compose.yml b/management-console/docker-compose.yml new file mode 100644 index 0000000..e311b7f --- /dev/null +++ b/management-console/docker-compose.yml @@ -0,0 +1,14 @@ +services: + management-console: + build: . + container_name: smart-recycling-console + restart: unless-stopped + ports: + - "3000:3000" + volumes: + - console-data:/app/.wrangler/state + environment: + - NODE_ENV=production + +volumes: + console-data: diff --git a/management-console/drizzle.config.ts b/management-console/drizzle.config.ts new file mode 100644 index 0000000..27464b7 --- /dev/null +++ b/management-console/drizzle.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "drizzle-kit"; + +export default defineConfig({ + dialect: "sqlite", + schema: "./db/schema.ts", + out: "./drizzle", +}); diff --git a/management-console/drizzle/0000_colorful_carmella_unuscione.sql b/management-console/drizzle/0000_colorful_carmella_unuscione.sql new file mode 100644 index 0000000..d38fda5 --- /dev/null +++ b/management-console/drizzle/0000_colorful_carmella_unuscione.sql @@ -0,0 +1,127 @@ +CREATE TABLE `alerts` ( + `id` text PRIMARY KEY NOT NULL, + `created_at` integer NOT NULL, + `type` text NOT NULL, + `severity` text NOT NULL, + `source` text NOT NULL, + `reference` text NOT NULL, + `description` text NOT NULL, + `status` text NOT NULL +); +--> statement-breakpoint +CREATE TABLE `audit_logs` ( + `id` text PRIMARY KEY NOT NULL, + `created_at` integer NOT NULL, + `actor` text NOT NULL, + `action` text NOT NULL, + `details` text NOT NULL, + `level` text NOT NULL +); +--> statement-breakpoint +CREATE TABLE `deliveries` ( + `record_id` text PRIMARY KEY NOT NULL, + `device_id` text NOT NULL, + `created_at` integer NOT NULL, + `employee_name` text NOT NULL, + `employee_no` text NOT NULL, + `machine_no` text NOT NULL, + `production_batch` text NOT NULL, + `garbage_type` integer NOT NULL, + `weight` real NOT NULL, + `payload` text NOT NULL, + `status` text NOT NULL +); +--> statement-breakpoint +CREATE TABLE `devices` ( + `id` text PRIMARY KEY NOT NULL, + `type` text NOT NULL, + `ip` text NOT NULL, + `status` text NOT NULL, + `last_seen` integer NOT NULL, + `heartbeat_seconds` integer NOT NULL, + `fill_percent` integer DEFAULT 0 NOT NULL, + `category` text DEFAULT '' NOT NULL +); +--> statement-breakpoint +CREATE TABLE `events` ( + `id` text PRIMARY KEY NOT NULL, + `operator` text NOT NULL, + `entered_at` integer NOT NULL, + `left_at` integer NOT NULL, + `atomic_count` integer NOT NULL, + `scale_total` real NOT NULL, + `verdict` text NOT NULL +); +--> statement-breakpoint +CREATE TABLE `people` ( + `id` text PRIMARY KEY NOT NULL, + `name` text NOT NULL, + `workshop` text NOT NULL, + `role` text NOT NULL, + `face_status` text NOT NULL, + `face_version` integer DEFAULT 1 NOT NULL, + `sync_status` text NOT NULL, + `active` integer DEFAULT true NOT NULL, + `updated_at` integer NOT NULL +); +--> statement-breakpoint +CREATE TABLE `reconciliations` ( + `id` text PRIMARY KEY NOT NULL, + `event_id` text NOT NULL, + `category` text NOT NULL, + `small_scale` real NOT NULL, + `floor_scale` real NOT NULL, + `difference` real NOT NULL, + `difference_rate` real NOT NULL, + `verdict` text NOT NULL, + `storage_status` text NOT NULL +); +--> statement-breakpoint +CREATE TABLE `rfid_tags` ( + `id` text PRIMARY KEY NOT NULL, + `status` text NOT NULL, + `name` text NOT NULL, + `last_checked` integer +); +--> statement-breakpoint +CREATE TABLE `settings` ( + `key` text PRIMARY KEY NOT NULL, + `value` text NOT NULL, + `updated_at` integer NOT NULL +); +--> statement-breakpoint +CREATE TABLE `traces` ( + `id` text PRIMARY KEY NOT NULL, + `created_at` integer NOT NULL, + `workshop` text NOT NULL, + `task_id` text NOT NULL, + `rfid` text NOT NULL, + `batch` text NOT NULL, + `tare` real NOT NULL, + `gross` real NOT NULL, + `net` real NOT NULL, + `machine` text NOT NULL, + `operator` text NOT NULL, + `mes_status` text NOT NULL +); +--> statement-breakpoint +CREATE TABLE `weighings` ( + `id` text PRIMARY KEY NOT NULL, + `created_at` integer NOT NULL, + `operator` text NOT NULL, + `machine` text NOT NULL, + `category` text NOT NULL, + `source` text NOT NULL, + `weight` real NOT NULL, + `event_id` text NOT NULL, + `status` text NOT NULL +); +--> statement-breakpoint +CREATE TABLE `workshops` ( + `code` text PRIMARY KEY NOT NULL, + `name` text NOT NULL, + `location` text NOT NULL, + `note` text NOT NULL +); +--> statement-breakpoint +CREATE UNIQUE INDEX `workshops_name_unique` ON `workshops` (`name`); \ No newline at end of file diff --git a/management-console/drizzle/meta/0000_snapshot.json b/management-console/drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000..c608281 --- /dev/null +++ b/management-console/drizzle/meta/0000_snapshot.json @@ -0,0 +1,780 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "086a5b90-1490-42d6-a241-39b8df513cd8", + "prevId": "00000000-0000-0000-0000-000000000000", + "tables": { + "alerts": { + "name": "alerts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reference": { + "name": "reference", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "actor": { + "name": "actor", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "deliveries": { + "name": "deliveries", + "columns": { + "record_id": { + "name": "record_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "device_id": { + "name": "device_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "employee_name": { + "name": "employee_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "employee_no": { + "name": "employee_no", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "machine_no": { + "name": "machine_no", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "production_batch": { + "name": "production_batch", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "garbage_type": { + "name": "garbage_type", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "weight": { + "name": "weight", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "devices": { + "name": "devices", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_seen": { + "name": "last_seen", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "heartbeat_seconds": { + "name": "heartbeat_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fill_percent": { + "name": "fill_percent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "events": { + "name": "events", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "operator": { + "name": "operator", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entered_at": { + "name": "entered_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "left_at": { + "name": "left_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "atomic_count": { + "name": "atomic_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scale_total": { + "name": "scale_total", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "verdict": { + "name": "verdict", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "people": { + "name": "people", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workshop": { + "name": "workshop", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "face_status": { + "name": "face_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "face_version": { + "name": "face_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "sync_status": { + "name": "sync_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "active": { + "name": "active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "reconciliations": { + "name": "reconciliations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "small_scale": { + "name": "small_scale", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "floor_scale": { + "name": "floor_scale", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "difference": { + "name": "difference", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "difference_rate": { + "name": "difference_rate", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "verdict": { + "name": "verdict", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "storage_status": { + "name": "storage_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rfid_tags": { + "name": "rfid_tags", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_checked": { + "name": "last_checked", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "traces": { + "name": "traces", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workshop": { + "name": "workshop", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rfid": { + "name": "rfid", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "batch": { + "name": "batch", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tare": { + "name": "tare", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "gross": { + "name": "gross", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "net": { + "name": "net", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "machine": { + "name": "machine", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "operator": { + "name": "operator", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mes_status": { + "name": "mes_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "weighings": { + "name": "weighings", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "operator": { + "name": "operator", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "machine": { + "name": "machine", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "weight": { + "name": "weight", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "workshops": { + "name": "workshops", + "columns": { + "code": { + "name": "code", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "location": { + "name": "location", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "workshops_name_unique": { + "name": "workshops_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/management-console/drizzle/meta/_journal.json b/management-console/drizzle/meta/_journal.json new file mode 100644 index 0000000..fdc2c65 --- /dev/null +++ b/management-console/drizzle/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "sqlite", + "entries": [ + { + "idx": 0, + "version": "6", + "when": 1785201529692, + "tag": "0000_colorful_carmella_unuscione", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/management-console/eslint.config.mjs b/management-console/eslint.config.mjs new file mode 100644 index 0000000..05e726d --- /dev/null +++ b/management-console/eslint.config.mjs @@ -0,0 +1,18 @@ +import { defineConfig, globalIgnores } from "eslint/config"; +import nextVitals from "eslint-config-next/core-web-vitals"; +import nextTs from "eslint-config-next/typescript"; + +const eslintConfig = defineConfig([ + ...nextVitals, + ...nextTs, + // Override default ignores of eslint-config-next. + globalIgnores([ + // Default ignores of eslint-config-next: + ".next/**", + "out/**", + "build/**", + "next-env.d.ts", + ]), +]); + +export default eslintConfig; diff --git a/management-console/examples/d1/app/api/notes/route.ts b/management-console/examples/d1/app/api/notes/route.ts new file mode 100644 index 0000000..07f8318 --- /dev/null +++ b/management-console/examples/d1/app/api/notes/route.ts @@ -0,0 +1,58 @@ +import { desc } from "drizzle-orm"; +import { getDb } from "../../../../../db"; +import { notes } from "../../../db/schema"; + +function toRouteErrorMessage(error: unknown) { + const message = error instanceof Error ? error.message : "Unexpected error"; + const detail = + error instanceof Error && error.cause instanceof Error ? error.cause.message : ""; + const combined = `${message}\n${detail}`; + + if (combined.includes("no such table") || combined.includes('from "notes"')) { + return "The notes table is unavailable. Generate the migration locally with `npm run db:generate`, then deploy so the platform can apply the generated SQL to the real D1 database."; + } + + return message; +} + +export async function GET() { + try { + const db = getDb(); + const rows = await db + .select() + .from(notes) + .orderBy(desc(notes.createdAt), desc(notes.id)) + .limit(20); + + return Response.json({ notes: rows }); + } catch (error) { + return Response.json( + { error: toRouteErrorMessage(error) }, + { status: 500 } + ); + } +} + +export async function POST(request: Request) { + try { + const payload = (await request.json()) as { + title?: string; + content?: string; + }; + const title = payload.title?.trim() ?? ""; + const content = payload.content?.trim() ?? ""; + + if (!title) { + return Response.json({ error: "title is required" }, { status: 400 }); + } + + const db = getDb(); + const [note] = await db.insert(notes).values({ title, content }).returning(); + return Response.json({ note }, { status: 201 }); + } catch (error) { + return Response.json( + { error: toRouteErrorMessage(error) }, + { status: 500 } + ); + } +} diff --git a/management-console/examples/d1/db/schema.ts b/management-console/examples/d1/db/schema.ts new file mode 100644 index 0000000..7bb0c15 --- /dev/null +++ b/management-console/examples/d1/db/schema.ts @@ -0,0 +1,9 @@ +import { sql } from "drizzle-orm"; +import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; + +export const notes = sqliteTable("notes", { + id: integer("id").primaryKey({ autoIncrement: true }), + title: text("title").notNull(), + content: text("content").notNull().default(""), + createdAt: text("created_at").notNull().default(sql`CURRENT_TIMESTAMP`), +}); diff --git a/management-console/next.config.ts b/management-console/next.config.ts new file mode 100644 index 0000000..e9ffa30 --- /dev/null +++ b/management-console/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/management-console/package-lock.json b/management-console/package-lock.json new file mode 100644 index 0000000..ea70c96 --- /dev/null +++ b/management-console/package-lock.json @@ -0,0 +1,11169 @@ +{ + "name": "smart-recycling-operations-console", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "smart-recycling-operations-console", + "version": "1.0.0", + "dependencies": { + "drizzle-orm": "0.45.2", + "next": "16.2.6", + "react": "19.2.6", + "react-dom": "19.2.6" + }, + "devDependencies": { + "@cloudflare/vite-plugin": "1.37.1", + "@tailwindcss/postcss": "4.2.1", + "@types/node": "22.19.19", + "@types/react": "19.2.14", + "@types/react-dom": "19.2.3", + "@vitejs/plugin-react": "6.0.2", + "@vitejs/plugin-rsc": "0.5.26", + "drizzle-kit": "0.31.10", + "eslint": "9.39.4", + "eslint-config-next": "16.2.6", + "react-server-dom-webpack": "19.2.6", + "tailwindcss": "4.2.1", + "typescript": "5.9.3", + "vinext": "0.0.50", + "vite": "8.0.13", + "wrangler": "4.92.0" + }, + "engines": { + "node": ">=22.13.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", + "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@cloudflare/kv-asset-handler": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.5.0.tgz", + "integrity": "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==", + "dev": true, + "license": "MIT OR Apache-2.0", + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@cloudflare/unenv-preset": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.16.1.tgz", + "integrity": "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==", + "dev": true, + "license": "MIT OR Apache-2.0", + "peerDependencies": { + "unenv": "2.0.0-rc.24", + "workerd": ">1.20260305.0 <2.0.0-0" + }, + "peerDependenciesMeta": { + "workerd": { + "optional": true + } + } + }, + "node_modules/@cloudflare/vite-plugin": { + "version": "1.37.1", + "resolved": "https://registry.npmjs.org/@cloudflare/vite-plugin/-/vite-plugin-1.37.1.tgz", + "integrity": "sha512-FldhsmkXyLsX3yI+p0aJDo/kGyY69pDdynz2JjcuH3eCiQ+G9XaY3CrN+UYEfCbWv8CiR2wL95lNg0fT/HfwWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cloudflare/unenv-preset": "2.16.1", + "miniflare": "4.20260515.0", + "unenv": "2.0.0-rc.24", + "wrangler": "4.92.0", + "ws": "8.18.0" + }, + "peerDependencies": { + "vite": "^6.1.0 || ^7.0.0 || ^8.0.0", + "wrangler": "^4.92.0" + } + }, + "node_modules/@cloudflare/workerd-darwin-64": { + "version": "1.20260515.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260515.1.tgz", + "integrity": "sha512-Wtw44el2pNbzixvTkWdfeBDTrQwQbJRz7/JUvPKV27I0pQWXbhNJPpM8cstq/pbrU5AGcA/HjFH6yPMRTIRKig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-darwin-arm64": { + "version": "1.20260515.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260515.1.tgz", + "integrity": "sha512-X8EqkZej6FfmhF9AVAQ3FhyQRr9acS4RcDunMU2YiuxKHF1IU8zzH3vY30/POaG+rUu9vGDp/VgUl49VPenHJQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20260515.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260515.1.tgz", + "integrity": "sha512-CDC89QxQ7Y7t7RG1Jd9vj/qolE1sQRkI2OSEuV5BMJi0vW/gV4OVG6xjpdK3b1OYnSWDzF7NpvlR5Yg86q7k4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20260515.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260515.1.tgz", + "integrity": "sha512-WxbW/PToYES4fvHXzsr/5qOiETQs/Z9iZ0mjSZAiEwq5cMLZemzGN0COx+uFb9OvQwzh6Pg159qPFnw3+i9FuA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20260515.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260515.1.tgz", + "integrity": "sha512-WmV/iv+MHjYsvkcMVzpM2B5/mf06UUkdpVhZrtMfV9graWjBGPYFvE/eab8748RPVGKh1Xe1vXofLzDSwc08lA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@drizzle-team/brocli": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz", + "integrity": "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild-kit/core-utils": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz", + "integrity": "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==", + "deprecated": "Merged into tsx: https://tsx.is", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.18.20", + "source-map-support": "^0.5.21" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/@esbuild-kit/esm-loader": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@esbuild-kit/esm-loader/-/esm-loader-2.6.5.tgz", + "integrity": "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==", + "deprecated": "Merged into tsx: https://tsx.is", + "dev": true, + "license": "MIT", + "dependencies": { + "@esbuild-kit/core-utils": "^3.3.2", + "get-tsconfig": "^4.7.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@next/env": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.6.tgz", + "integrity": "sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw==", + "license": "MIT" + }, + "node_modules/@next/eslint-plugin-next": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.2.6.tgz", + "integrity": "sha512-Z8l6o4JWKUl755x4R+wogD86KPeU+Ckw4K+SYG4kHeOJtRenDeK+OSbGcqZpDtbwn9DsJVdir2UxmwXuinUbUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "3.3.1" + } + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.6.tgz", + "integrity": "sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.6.tgz", + "integrity": "sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.6.tgz", + "integrity": "sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.6.tgz", + "integrity": "sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.6.tgz", + "integrity": "sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.6.tgz", + "integrity": "sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.6.tgz", + "integrity": "sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.6.tgz", + "integrity": "sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.4.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.130.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.130.0.tgz", + "integrity": "sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@poppinss/colors": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz", + "integrity": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^4.1.5" + } + }, + "node_modules/@poppinss/dumper": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.5.tgz", + "integrity": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@sindresorhus/is": "^7.0.2", + "supports-color": "^10.0.0" + } + }, + "node_modules/@poppinss/dumper/node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/@poppinss/exception": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.3.tgz", + "integrity": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@resvg/resvg-wasm": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@resvg/resvg-wasm/-/resvg-wasm-2.4.0.tgz", + "integrity": "sha512-C7c51Nn4yTxXFKvgh2txJFNweaVcfUPQxwEUFw4aWsCmfiBDJsTSwviIF8EcwjQ6k8bPyMWCl1vw4BdxE569Cg==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.1.tgz", + "integrity": "sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.1.tgz", + "integrity": "sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.1.tgz", + "integrity": "sha512-YKrVwQjIRBPo+5G/u03wGjbdy4q7pyzCe93DK9VJ7zkVmeg8LJ7GbgsiHWdR4xSoe4CAXRD7Bcjgbtr64bkXNg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.1.tgz", + "integrity": "sha512-z/oBsREo46SsFqBwYtFe0kpJeBijAT48O/WXLI4suiCLBkr03RTtTJMCzSdDd2znlh8VJizL09XVkQgk8IZonw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.1.tgz", + "integrity": "sha512-ik8q7GM11zxvYxFc2PeDcT6TBvhCQMaUxfph/M5l9sKuTs/Sjg3L+Byw0F7w0ZVLBZmx30P+gG0ECzzN+MFcmQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.1.tgz", + "integrity": "sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.1.tgz", + "integrity": "sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.1.tgz", + "integrity": "sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.1.tgz", + "integrity": "sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.1.tgz", + "integrity": "sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.1.tgz", + "integrity": "sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.1.tgz", + "integrity": "sha512-/Mh0Zhq3OP7fVs0kcQHZP6lZEthMGTaSf8UBQYSFEZDWGXXlEC+nJ6EqenaK2t4LBXMe3A+K/G2BVXXdtOr4PQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.1.tgz", + "integrity": "sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.1.tgz", + "integrity": "sha512-1D+UqZdfnuR+Jy1GgMJwi85bD40H21uNmOPRWQhw4oRSuolZ/B5rixZ45DK2KXOTCvmVCecauWgEhbw8bI7tOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.1.tgz", + "integrity": "sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@shuding/opentype.js": { + "version": "1.4.0-beta.0", + "resolved": "https://registry.npmjs.org/@shuding/opentype.js/-/opentype.js-1.4.0-beta.0.tgz", + "integrity": "sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fflate": "^0.7.3", + "string.prototype.codepointat": "^0.2.1" + }, + "bin": { + "ot": "bin/ot" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/@sindresorhus/is": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", + "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@speed-highlight/core": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.15.tgz", + "integrity": "sha512-BMq1K3DsElxDWawkX6eLg9+CKJrTVGCBAWVuHXVUV2u0s2711qiChLSId6ikYPfxhdYocLNt3wWwSvDiTvFabw==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz", + "integrity": "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.31.1", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.1" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.1.tgz", + "integrity": "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.2.1", + "@tailwindcss/oxide-darwin-arm64": "4.2.1", + "@tailwindcss/oxide-darwin-x64": "4.2.1", + "@tailwindcss/oxide-freebsd-x64": "4.2.1", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", + "@tailwindcss/oxide-linux-x64-musl": "4.2.1", + "@tailwindcss/oxide-wasm32-wasi": "4.2.1", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.1.tgz", + "integrity": "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.1.tgz", + "integrity": "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.1.tgz", + "integrity": "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.1.tgz", + "integrity": "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.1.tgz", + "integrity": "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.1.tgz", + "integrity": "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.1.tgz", + "integrity": "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.1.tgz", + "integrity": "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.1.tgz", + "integrity": "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.1.tgz", + "integrity": "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.8.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.8.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { + "version": "2.8.1", + "dev": true, + "inBundle": true, + "license": "0BSD", + "optional": true + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.1.tgz", + "integrity": "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.1.tgz", + "integrity": "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.2.1.tgz", + "integrity": "sha512-OEwGIBnXnj7zJeonOh6ZG9woofIjGrd2BORfvE5p9USYKDCZoQmfqLcfNiRWoJlRWLdNPn2IgVZuWAOM4iTYMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.2.1", + "@tailwindcss/oxide": "4.2.1", + "postcss": "^8.5.6", + "tailwindcss": "4.2.1" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", + "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.3.tgz", + "integrity": "sha512-PwFvSKsXGShKGW6n5bZOhGHEcCZXM8HofLK9fNsEwZXzFRjoY+XT1Vsf1zgyXdwTr0ZYz1/2tkZ0DBTT9jZjhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.59.3", + "@typescript-eslint/type-utils": "8.59.3", + "@typescript-eslint/utils": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.59.3", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.3.tgz", + "integrity": "sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.59.3", + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.3.tgz", + "integrity": "sha512-ECiUWa/KYRGDFUqTNehaRgzDshnJfkTABJxVemHk4ko22gcr0ukloKjWvyQ64g8YCV/UI47kN1dbmjf/GaQYng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.59.3", + "@typescript-eslint/types": "^8.59.3", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.3.tgz", + "integrity": "sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.3.tgz", + "integrity": "sha512-PcIJHjmaREXLgIAIzLnSY9VucEzz8FKXsRgFa1DmdGCK/5tJpW03TKJF01Q6VZd1lLdz2sIKPWaDUZN9dp//dw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.3.tgz", + "integrity": "sha512-g71d8QD8UaiHGvrJwyIS1hCX5r63w6Jll+4VEYhEAHXTDIqX1JgxhTAbEHtKntL9kuc4jRo7/GWw5xfCepSccQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3", + "@typescript-eslint/utils": "8.59.3", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.3.tgz", + "integrity": "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.3.tgz", + "integrity": "sha512-CbRjVRAf7Lr9Kr8RopKcbY45p2VfmmHrm0ygOCYFi7oU8q19m0Fs/6iHS7kNOmwpp+ob07ZVcAqlxUod9lYdmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.59.3", + "@typescript-eslint/tsconfig-utils": "8.59.3", + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.3.tgz", + "integrity": "sha512-JAvT14goBzRzzzZyqq3P9BLArIxTtQURUtFgQ/V7FO+eU+Gg6ES+5ymOPP1wRxXcxAYeivCk4uS3jCKWI1K8Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.59.3", + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.3.tgz", + "integrity": "sha512-f1UQF7ggd42YiwI5wGrRaPsa+P0CINBlrkLPmGfpq/u/I/oVtecoEIfFR9ag/oa1sLOsRNZ6xehf6qMZhQGBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.3", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@unpic/core": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@unpic/core/-/core-1.0.3.tgz", + "integrity": "sha512-aum9YNVUGso7MjGLD0Rp/08kywCGLqZ03/q6VQBFFakDBOXWEc8D4kPGcZ8v5wEnGRex3lE+++bOuucBp3KJ/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "unpic": "^4.2.2" + } + }, + "node_modules/@unpic/react": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@unpic/react/-/react-1.0.2.tgz", + "integrity": "sha512-5RmRfELwTF8w+4zjtQGqjpvX+RU2VLvis3xDCS1O2uWk0PZN2cvatL+3/KAR3mshAuRrkFGTX1XwyAezSXaoCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unpic/core": "^1.0.3" + }, + "peerDependencies": { + "next": "^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "next": { + "optional": true + } + } + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@vercel/og": { + "version": "0.8.6", + "resolved": "https://registry.npmjs.org/@vercel/og/-/og-0.8.6.tgz", + "integrity": "sha512-hBcWIOppZV14bi+eAmCZj8Elj8hVSUZJTpf1lgGBhVD85pervzQ1poM/qYfFUlPraYSZYP+ASg6To5BwYmUSGQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "@resvg/resvg-wasm": "2.4.0", + "satori": "0.16.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.2.tgz", + "integrity": "sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/@vitejs/plugin-rsc": { + "version": "0.5.26", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-rsc/-/plugin-rsc-0.5.26.tgz", + "integrity": "sha512-T8W8ODEutblw9qXQB512LDPyv1tAbJRD/Gf0QEGsAoydl4nxEtIrghnhoI9oLY9R+7aw+cLk1ZEltxWHWf4aHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.18", + "es-module-lexer": "^2.1.0", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21", + "srvx": "^0.11.15", + "strip-literal": "^3.1.0", + "turbo-stream": "^3.2.0", + "vitefu": "^1.1.3" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*", + "react-server-dom-webpack": "*", + "vite": "*" + }, + "peerDependenciesMeta": { + "react-server-dom-webpack": { + "optional": true + } + } + }, + "node_modules/@vitejs/plugin-rsc/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.18.tgz", + "integrity": "sha512-CUY5Mnhe64xQBGZEEXQ5WyZwsc1JU3vAZLIxtrsBt3LO6UOb+C8GunVKqe9sT8NeWb4lqSaoJtp2xo6GxT1MNw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-loose": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/acorn-loose/-/acorn-loose-8.5.2.tgz", + "integrity": "sha512-PPvV6g8UGMGgjrMu+n/f9E/tCSkNQ2Y97eFvuVdJfG11+xdIeDcLyNdC8SHcrHbRqkfwLASdplyR6B6sKM1U4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.15.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axe-core": { + "version": "4.11.4", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.4.tgz", + "integrity": "sha512-KunSNx+TVpkAw/6ULfhnx+HWRecjqZGTOyquAoWHYLRSdK1tB5Ihce1ZW+UY3fj33bYAFWPu7W/GRSmmrCGuxA==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", + "integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.30", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.30.tgz", + "integrity": "sha512-xjOFN16Ha1+Rz4nFYKqHU/LSB+gx/Vi3yQLX7r7sAW+Wa+8hhF2h4pvqTrTMc8+WcDBEunnUurr46Jvv0jk3Vg==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/blake3-wasm": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", + "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001793", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", + "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-background-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/css-background-parser/-/css-background-parser-0.1.0.tgz", + "integrity": "sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA==", + "dev": true, + "license": "MIT" + }, + "node_modules/css-box-shadow": { + "version": "1.0.0-3", + "resolved": "https://registry.npmjs.org/css-box-shadow/-/css-box-shadow-1.0.0-3.tgz", + "integrity": "sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg==", + "dev": true, + "license": "MIT" + }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=4" + } + }, + "node_modules/css-gradient-parser": { + "version": "0.0.16", + "resolved": "https://registry.npmjs.org/css-gradient-parser/-/css-gradient-parser-0.0.16.tgz", + "integrity": "sha512-3O5QdqgFRUbXvK1x5INf1YkBz1UKSWqrd63vWsum8MNHDBYD5urm3QtxZbKU259OrEXNM26lP/MPY3d1IGkBgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/drizzle-kit": { + "version": "0.31.10", + "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.10.tgz", + "integrity": "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@drizzle-team/brocli": "^0.10.2", + "@esbuild-kit/esm-loader": "^2.5.5", + "esbuild": "^0.25.4", + "tsx": "^4.21.0" + }, + "bin": { + "drizzle-kit": "bin.cjs" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/drizzle-orm": { + "version": "0.45.2", + "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.45.2.tgz", + "integrity": "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==", + "license": "Apache-2.0", + "peerDependencies": { + "@aws-sdk/client-rds-data": ">=3", + "@cloudflare/workers-types": ">=4", + "@electric-sql/pglite": ">=0.2.0", + "@libsql/client": ">=0.10.0", + "@libsql/client-wasm": ">=0.10.0", + "@neondatabase/serverless": ">=0.10.0", + "@op-engineering/op-sqlite": ">=2", + "@opentelemetry/api": "^1.4.1", + "@planetscale/database": ">=1.13", + "@prisma/client": "*", + "@tidbcloud/serverless": "*", + "@types/better-sqlite3": "*", + "@types/pg": "*", + "@types/sql.js": "*", + "@upstash/redis": ">=1.34.7", + "@vercel/postgres": ">=0.8.0", + "@xata.io/client": "*", + "better-sqlite3": ">=7", + "bun-types": "*", + "expo-sqlite": ">=14.0.0", + "gel": ">=2", + "knex": "*", + "kysely": "*", + "mysql2": ">=2", + "pg": ">=8", + "postgres": ">=3", + "sql.js": ">=1", + "sqlite3": ">=5" + }, + "peerDependenciesMeta": { + "@aws-sdk/client-rds-data": { + "optional": true + }, + "@cloudflare/workers-types": { + "optional": true + }, + "@electric-sql/pglite": { + "optional": true + }, + "@libsql/client": { + "optional": true + }, + "@libsql/client-wasm": { + "optional": true + }, + "@neondatabase/serverless": { + "optional": true + }, + "@op-engineering/op-sqlite": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@prisma/client": { + "optional": true + }, + "@tidbcloud/serverless": { + "optional": true + }, + "@types/better-sqlite3": { + "optional": true + }, + "@types/pg": { + "optional": true + }, + "@types/sql.js": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/postgres": { + "optional": true + }, + "@xata.io/client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "bun-types": { + "optional": true + }, + "expo-sqlite": { + "optional": true + }, + "gel": { + "optional": true + }, + "knex": { + "optional": true + }, + "kysely": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "postgres": { + "optional": true + }, + "prisma": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + } + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.358", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.358.tgz", + "integrity": "sha512-EO7tKm3QxRqTs1lSuPXzl6yRAwznehp0AH9OoMOIC+4mQzTFday8FJCO5KU6J/TFSQXEOahNq4vTKpz1jmCVOA==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex-xs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-2.0.1.tgz", + "integrity": "sha512-1QFuh8l7LqUcKe24LsPUNzjrzJQ7pgRwp1QMcZ5MX6mFplk2zQ08NVCM84++1cveaUUYtcCYHmeFEuNg16sU4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.21.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.3.tgz", + "integrity": "sha512-QyL119InA+XXEkNLNTPCXPugSvOfhwv0JOlGNzvxs0hZaiHLNvXSpudUWsOlsXGWJh8G6ckCScEkVHfX3kw/2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/error-stack-parser-es": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", + "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/es-abstract": { + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.3.2.tgz", + "integrity": "sha512-HVLACW1TppGYjJ8H6/jqH/pqOtKRw6wMlrB23xfExmFWxFquAIWCmwoLsOyN96K4a5KbmOf5At9ZUO3GZbetAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.1.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.3.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.5", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/esbuild": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-next": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.2.6.tgz", + "integrity": "sha512-z2ELYSkyrrJ6cuunTU8vhsT/RpouPkjaSah06nVW6Rg2Hpg0Vs8s497/e5s8G8qtdp4ccsiovz5P1rv+5VSW2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@next/eslint-plugin-next": "16.2.6", + "eslint-import-resolver-node": "^0.3.6", + "eslint-import-resolver-typescript": "^3.5.2", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-jsx-a11y": "^6.10.0", + "eslint-plugin-react": "^7.37.0", + "eslint-plugin-react-hooks": "^7.0.0", + "globals": "16.4.0", + "typescript-eslint": "^8.46.0" + }, + "peerDependencies": { + "eslint": ">=9.0.0", + "typescript": ">=3.3.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-config-next/node_modules/globals": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.10.tgz", + "integrity": "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.16.1", + "resolve": "^2.0.0-next.6" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", + "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.4.0", + "get-tsconfig": "^4.10.0", + "is-bun-module": "^2.0.0", + "stable-hash": "^0.0.5", + "tinyglobby": "^0.2.13", + "unrs-resolver": "^1.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-resolver-typescript" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "aria-query": "^5.3.2", + "array-includes": "^3.1.8", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "hasown": "^2.0.2", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.1" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fflate": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.7.4.tgz", + "integrity": "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true, + "license": "MIT" + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/hex-rgb": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/hex-rgb/-/hex-rgb-4.3.0.tgz", + "integrity": "sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/image-size": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-2.0.2.tgz", + "integrity": "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w==", + "dev": true, + "license": "MIT", + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=16.x" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ipaddr.js": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.4.0.tgz", + "integrity": "sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bun-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", + "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.7.1" + } + }, + "node_modules/is-bun-module/node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", + "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.31.1", + "lightningcss-darwin-arm64": "1.31.1", + "lightningcss-darwin-x64": "1.31.1", + "lightningcss-freebsd-x64": "1.31.1", + "lightningcss-linux-arm-gnueabihf": "1.31.1", + "lightningcss-linux-arm64-gnu": "1.31.1", + "lightningcss-linux-arm64-musl": "1.31.1", + "lightningcss-linux-x64-gnu": "1.31.1", + "lightningcss-linux-x64-musl": "1.31.1", + "lightningcss-win32-arm64-msvc": "1.31.1", + "lightningcss-win32-x64-msvc": "1.31.1" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz", + "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz", + "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz", + "integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz", + "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz", + "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz", + "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz", + "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz", + "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz", + "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz", + "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz", + "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/linebreak": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz", + "integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "base64-js": "0.0.8", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/loader-runner": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz", + "integrity": "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/miniflare": { + "version": "4.20260515.0", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260515.0.tgz", + "integrity": "sha512-2j0oQWizk1Eu4Cm8tDX7Z+Nsjd0nebIj1TQcQ+Oy1QKeo0Ay9+bdn8wfLAtOj9znDCybDCUlnS1+nYvKXEdfNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "0.8.1", + "sharp": "^0.34.5", + "undici": "7.24.8", + "workerd": "1.20260515.1", + "ws": "8.18.0", + "youch": "4.1.0-beta.10" + }, + "bin": { + "miniflare": "bootstrap.js" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/next": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/next/-/next-16.2.6.tgz", + "integrity": "sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw==", + "license": "MIT", + "dependencies": { + "@next/env": "16.2.6", + "@swc/helpers": "0.5.15", + "baseline-browser-mapping": "^2.9.19", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=20.9.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "16.2.6", + "@next/swc-darwin-x64": "16.2.6", + "@next/swc-linux-arm64-gnu": "16.2.6", + "@next/swc-linux-arm64-musl": "16.2.6", + "@next/swc-linux-x64-gnu": "16.2.6", + "@next/swc-linux-x64-musl": "16.2.6", + "@next/swc-win32-arm64-msvc": "16.2.6", + "@next/swc-win32-x64-msvc": "16.2.6", + "sharp": "^0.34.5" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/node-exports-info": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", + "integrity": "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/node-releases": { + "version": "2.0.44", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.44.tgz", + "integrity": "sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", + "dev": true, + "license": "MIT" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-css-color": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/parse-css-color/-/parse-css-color-0.2.1.tgz", + "integrity": "sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "^1.1.4", + "hex-rgb": "^4.1.0" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", + "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", + "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.6" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-server-dom-webpack": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react-server-dom-webpack/-/react-server-dom-webpack-19.2.6.tgz", + "integrity": "sha512-762YXjBPc6hw2V16k4x1sdnw0mxghcSPzoiOhefGYjiTguJLCImGBUz6EjznPIfqKhWgLtpaQMlBhp66N8dKrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn-loose": "^8.3.0", + "neo-async": "^2.6.1", + "webpack-sources": "^3.2.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "react": "^19.2.6", + "react-dom": "^19.2.6", + "webpack": "^5.59.0" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "2.0.0-next.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", + "integrity": "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.2", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rolldown": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.1.tgz", + "integrity": "sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.130.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.1", + "@rolldown/binding-darwin-arm64": "1.0.1", + "@rolldown/binding-darwin-x64": "1.0.1", + "@rolldown/binding-freebsd-x64": "1.0.1", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.1", + "@rolldown/binding-linux-arm64-gnu": "1.0.1", + "@rolldown/binding-linux-arm64-musl": "1.0.1", + "@rolldown/binding-linux-ppc64-gnu": "1.0.1", + "@rolldown/binding-linux-s390x-gnu": "1.0.1", + "@rolldown/binding-linux-x64-gnu": "1.0.1", + "@rolldown/binding-linux-x64-musl": "1.0.1", + "@rolldown/binding-openharmony-arm64": "1.0.1", + "@rolldown/binding-wasm32-wasi": "1.0.1", + "@rolldown/binding-win32-arm64-msvc": "1.0.1", + "@rolldown/binding-win32-x64-msvc": "1.0.1" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", + "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/satori": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/satori/-/satori-0.16.0.tgz", + "integrity": "sha512-ZvHN3ygzZ8FuxjSNB+mKBiF/NIoqHzlBGbD0MJiT+MvSsFOvotnWOhdTjxKzhHRT2wPC1QbhLzx2q/Y83VhfYQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "@shuding/opentype.js": "1.4.0-beta.0", + "css-background-parser": "^0.1.0", + "css-box-shadow": "1.0.0-3", + "css-gradient-parser": "^0.0.16", + "css-to-react-native": "^3.0.0", + "emoji-regex-xs": "^2.0.1", + "escape-html": "^1.0.3", + "linebreak": "^1.1.0", + "parse-css-color": "^0.2.1", + "postcss-value-parser": "^4.2.0", + "yoga-layout": "^3.2.1" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/schema-utils/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/schema-utils/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/schema-utils/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/sharp/node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "devOptional": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/srvx": { + "version": "0.11.15", + "resolved": "https://registry.npmjs.org/srvx/-/srvx-0.11.15.tgz", + "integrity": "sha512-iXsux0UcOjdvs0LCMa2Ws3WwcDUozA3JN3BquNXkaFPP7TpRqgunKdEgoZ/uwb1J6xaYHfxtz9Twlh6yzwM6Tg==", + "dev": true, + "license": "MIT", + "bin": { + "srvx": "bin/srvx.mjs" + }, + "engines": { + "node": ">=20.16.0" + } + }, + "node_modules/stable-hash": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", + "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.codepointat": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz", + "integrity": "sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==", + "dev": true, + "license": "MIT" + }, + "node_modules/string.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz", + "integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser": { + "version": "5.47.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.47.1.tgz", + "integrity": "sha512-tPbLXTI6ohPASb/1YViL428oEHu6/qv1OxqYnfaonVCFHqx4+wCd95pHrQWsL5X4pl90CTyW9piSAsS2L0VoMw==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.6.0.tgz", + "integrity": "sha512-Eum+5ajkaOhf5KbM26osvv21kLD7BaGqQ1UA4Ami4arYwylmGUQTgHFpHDdmJod1q4QXa66p0to/FBKID+J1vA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "html-minifier-terser": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tsconfck": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz", + "integrity": "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==", + "dev": true, + "license": "MIT", + "bin": { + "tsconfck": "bin/tsconfck.js" + }, + "engines": { + "node": "^18 || >=20" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.1.tgz", + "integrity": "sha512-TvncJykhxAzFCk0VQZKBTClall4Pm7qXDSodb6uxi8QFa8X8mT6ABjxxsQ2opDRYxG7AzcRWXaFtruz5HJKuWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/turbo-stream": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-3.2.0.tgz", + "integrity": "sha512-EK+bZ9UVrVh7JLslVFOV0GEMsociOqVOvEMTAd4ixMyffN5YNIEdLZWXUx5PJqDbTxSIBWw04HS9gCY4frYQDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.3.tgz", + "integrity": "sha512-KgusgyDgG4LI8Ih/sWaCtZ06tckLAS5CvT5A4D1Q7bYVoAAyzwiZvE4BmwDHkhRVkvhRBepKeASoFzQetha7Fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.59.3", + "@typescript-eslint/parser": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3", + "@typescript-eslint/utils": "8.59.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.8.tgz", + "integrity": "sha512-6KQ/+QxK49Z/p3HO6E5ZCZWNnCasyZLa5ExaVYyvPxUwKtbCPMKELJOqh7EqOle0t9cH/7d2TaaTRRa6Nhs4YQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unenv": { + "version": "2.0.0-rc.24", + "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz", + "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pathe": "^2.0.3" + } + }, + "node_modules/unicode-trie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, + "node_modules/unpic": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/unpic/-/unpic-4.2.2.tgz", + "integrity": "sha512-z6T2ScMgRV2y2H8MwwhY5xHZWXhUx/YxtOCGJwfURSl7ypVy4HpLIMWoIZKnnxQa/RKzM0kg8hUh0paIrpLfvw==", + "dev": true, + "license": "MIT" + }, + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vinext": { + "version": "0.0.50", + "resolved": "https://registry.npmjs.org/vinext/-/vinext-0.0.50.tgz", + "integrity": "sha512-uo72YNnq94NtogETWnhMdFSrkMLwWgeXh5PS6qh8ksajuvAaZX50bXYJ4a6dERQ/AnnXAlNByAGHCjjNxQrvig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unpic/react": "^1.0.2", + "@vercel/og": "^0.8.6", + "image-size": "2.0.2", + "ipaddr.js": "^2.1.0", + "magic-string": "^0.30.21", + "vite-plugin-commonjs": "^0.10.4", + "vite-tsconfig-paths": "^6.1.1", + "web-vitals": "^4.2.4" + }, + "bin": { + "vinext": "dist/cli.js" + }, + "engines": { + "node": ">=22" + }, + "peerDependencies": { + "@mdx-js/rollup": "^3.0.0", + "@vitejs/plugin-react": "^5.1.4 || ^6.0.0", + "@vitejs/plugin-rsc": "^0.5.23", + "react": "^19.2.6", + "react-dom": "^19.2.6", + "react-server-dom-webpack": "^19.2.6", + "vite": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@mdx-js/rollup": { + "optional": true + }, + "@vitejs/plugin-rsc": { + "optional": true + }, + "react-server-dom-webpack": { + "optional": true + } + } + }, + "node_modules/vite": { + "version": "8.0.13", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.13.tgz", + "integrity": "sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.14", + "rolldown": "1.0.1", + "tinyglobby": "^0.2.16" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-plugin-commonjs": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/vite-plugin-commonjs/-/vite-plugin-commonjs-0.10.4.tgz", + "integrity": "sha512-eWQuvQKCcx0QYB5e5xfxBNjQKyrjEWZIR9UOkOV6JAgxVhtbZvCOF+FNC2ZijBJ3U3Px04ZMMyyMyFBVWIJ5+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.12.1", + "magic-string": "^0.30.11", + "vite-plugin-dynamic-import": "^1.6.0" + } + }, + "node_modules/vite-plugin-dynamic-import": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/vite-plugin-dynamic-import/-/vite-plugin-dynamic-import-1.6.0.tgz", + "integrity": "sha512-TM0sz70wfzTIo9YCxVFwS8OA9lNREsh+0vMHGSkWDTZ7bgd1Yjs5RV8EgB634l/91IsXJReg0xtmuQqP0mf+rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.12.1", + "es-module-lexer": "^1.5.4", + "fast-glob": "^3.3.2", + "magic-string": "^0.30.11" + } + }, + "node_modules/vite-plugin-dynamic-import/node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite-plugin-dynamic-import/node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/vite-plugin-dynamic-import/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/vite-tsconfig-paths": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-6.1.1.tgz", + "integrity": "sha512-2cihq7zliibCCZ8P9cKJrQBkfgdvcFkOOc3Y02o3GWUDLgqjWsZudaoiuOwO/gzTzy17cS5F7ZPo4bsnS4DGkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "globrex": "^0.1.2", + "tsconfck": "^3.0.3" + }, + "peerDependencies": { + "vite": "*" + } + }, + "node_modules/vite/node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/vite/node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitefu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", + "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==", + "dev": true, + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/watchpack": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/web-vitals": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz", + "integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/webpack": { + "version": "5.106.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.106.2.tgz", + "integrity": "sha512-wGN3qcrBQIFmQ/c0AiOAQBvrZ5lmY8vbbMv4Mxfgzqd/B6+9pXtLo73WuS1dSGXM5QYY3hZnIbvx+K1xxe6FyA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.16.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.20.0", + "es-module-lexer": "^2.0.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "loader-runner": "^4.3.1", + "mime-db": "^1.54.0", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.17", + "watchpack": "^2.5.1", + "webpack-sources": "^3.3.4" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-sources": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.4.1.tgz", + "integrity": "sha512-eACpxRN02yaawnt+uUNIF7Qje6A9zArxBbcAJjK1PK3S9Ycg5jIuJ8pW4q8EMnwNZCEGltcjkRx1QzOxOkKD8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/webpack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/workerd": { + "version": "1.20260515.1", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260515.1.tgz", + "integrity": "sha512-MjKOJLcvU45xXedQowvuiHtJTxu4WTHYQeIlF7YmjuqhiI6dImTFxWCEoRQHiskztxuVSNEmdO7/0UfDu6OMnQ==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "bin": { + "workerd": "bin/workerd" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@cloudflare/workerd-darwin-64": "1.20260515.1", + "@cloudflare/workerd-darwin-arm64": "1.20260515.1", + "@cloudflare/workerd-linux-64": "1.20260515.1", + "@cloudflare/workerd-linux-arm64": "1.20260515.1", + "@cloudflare/workerd-windows-64": "1.20260515.1" + } + }, + "node_modules/wrangler": { + "version": "4.92.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.92.0.tgz", + "integrity": "sha512-/DKpQHPxkuZbQsO9dFW2700VTD/4DSZMHjy92fO/frNoDRi/zQsFCAd2ONCV6TGqcUoXcP3D8Bo2gj/L4M0qQQ==", + "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "@cloudflare/kv-asset-handler": "0.5.0", + "@cloudflare/unenv-preset": "2.16.1", + "blake3-wasm": "2.1.5", + "esbuild": "0.27.3", + "miniflare": "4.20260515.0", + "path-to-regexp": "6.3.0", + "unenv": "2.0.0-rc.24", + "workerd": "1.20260515.1" + }, + "bin": { + "wrangler": "bin/wrangler.js", + "wrangler2": "bin/wrangler.js" + }, + "engines": { + "node": ">=22.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@cloudflare/workers-types": "^4.20260515.1" + }, + "peerDependenciesMeta": { + "@cloudflare/workers-types": { + "optional": true + } + } + }, + "node_modules/wrangler/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoga-layout": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz", + "integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/youch": { + "version": "4.1.0-beta.10", + "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz", + "integrity": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@poppinss/dumper": "^0.6.4", + "@speed-highlight/core": "^1.2.7", + "cookie": "^1.0.2", + "youch-core": "^0.3.3" + } + }, + "node_modules/youch-core": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz", + "integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/exception": "^1.2.2", + "error-stack-parser-es": "^1.0.5" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/management-console/package.json b/management-console/package.json new file mode 100644 index 0000000..47a225a --- /dev/null +++ b/management-console/package.json @@ -0,0 +1,41 @@ +{ + "name": "smart-recycling-operations-console", + "version": "1.0.0", + "private": true, + "engines": { + "node": ">=22.13.0" + }, + "scripts": { + "dev": "WRANGLER_LOG_PATH=.wrangler/wrangler.log vinext dev", + "build": "WRANGLER_LOG_PATH=.wrangler/wrangler.log vinext build", + "start": "WRANGLER_LOG_PATH=.wrangler/wrangler.log wrangler dev --config dist/server/wrangler.json --ip 0.0.0.0 --port 3000", + "test": "npm run build && node --test tests/rendered-html.test.mjs", + "lint": "eslint . --ignore-pattern dist --ignore-pattern .next", + "db:generate": "drizzle-kit generate" + }, + "dependencies": { + "drizzle-orm": "0.45.2", + "next": "16.2.6", + "react": "19.2.6", + "react-dom": "19.2.6" + }, + "devDependencies": { + "@cloudflare/vite-plugin": "1.37.1", + "@tailwindcss/postcss": "4.2.1", + "@types/node": "22.19.19", + "@types/react": "19.2.14", + "@types/react-dom": "19.2.3", + "@vitejs/plugin-react": "6.0.2", + "@vitejs/plugin-rsc": "0.5.26", + "drizzle-kit": "0.31.10", + "eslint": "9.39.4", + "eslint-config-next": "16.2.6", + "react-server-dom-webpack": "19.2.6", + "tailwindcss": "4.2.1", + "typescript": "5.9.3", + "vinext": "0.0.50", + "vite": "8.0.13", + "wrangler": "4.92.0" + }, + "type": "module" +} diff --git a/management-console/postcss.config.mjs b/management-console/postcss.config.mjs new file mode 100644 index 0000000..61e3684 --- /dev/null +++ b/management-console/postcss.config.mjs @@ -0,0 +1,7 @@ +const config = { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; + +export default config; diff --git a/management-console/public/favicon.svg b/management-console/public/favicon.svg new file mode 100644 index 0000000..ca747cb --- /dev/null +++ b/management-console/public/favicon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/management-console/public/file.svg b/management-console/public/file.svg new file mode 100644 index 0000000..004145c --- /dev/null +++ b/management-console/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/management-console/public/globe.svg b/management-console/public/globe.svg new file mode 100644 index 0000000..567f17b --- /dev/null +++ b/management-console/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/management-console/public/og.png b/management-console/public/og.png new file mode 100644 index 0000000..1d87aae Binary files /dev/null and b/management-console/public/og.png differ diff --git a/management-console/public/window.svg b/management-console/public/window.svg new file mode 100644 index 0000000..b2b2a44 --- /dev/null +++ b/management-console/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/management-console/tests/rendered-html.test.mjs b/management-console/tests/rendered-html.test.mjs new file mode 100644 index 0000000..99ecaab --- /dev/null +++ b/management-console/tests/rendered-html.test.mjs @@ -0,0 +1,45 @@ +import assert from "node:assert/strict"; +import { access, readFile, readdir } from "node:fs/promises"; +import test from "node:test"; + +test("builds the finished operations console assets", async () => { + const [page, layout, clientFiles, hosting] = await Promise.all([ + readFile(new URL("../app/page.tsx", import.meta.url), "utf8"), + readFile(new URL("../app/layout.tsx", import.meta.url), "utf8"), + readdir(new URL("../dist/client/assets/", import.meta.url)), + readFile(new URL("../dist/.openai/hosting.json", import.meta.url), "utf8"), + ]); + + assert.match(page, /AdminConsole/); + assert.match(layout, /智能回收运营平台/); + assert.match(layout, /og\.png/); + assert.ok(clientFiles.some((file) => file.startsWith("AdminConsole-") && file.endsWith(".js"))); + assert.equal(JSON.parse(hosting).d1, "DB"); + await access(new URL("../dist/client/og.png", import.meta.url)); +}); + +test("removes starter preview and includes persistent API routes", async () => { + const [client, consoleApi, deviceApi, packageJson, readme] = await Promise.all([ + readFile(new URL("../app/AdminConsole.tsx", import.meta.url), "utf8"), + readFile(new URL("../app/api/console/route.ts", import.meta.url), "utf8"), + readFile(new URL("../app/api/v1/[...path]/route.ts", import.meta.url), "utf8"), + readFile(new URL("../package.json", import.meta.url), "utf8"), + readFile(new URL("../README.md", import.meta.url), "utf8"), + ]); + + assert.match(client, /废料称重/); + assert.match(client, /地磅核对/); + assert.match(client, /导体追溯/); + assert.match(client, /人员设备/); + assert.match(client, /终端模拟/); + assert.match(consoleApi, /person\.face/); + assert.match(consoleApi, /alert\.status/); + assert.match(deviceApi, /heartbeat/); + assert.match(deviceApi, /deliveries/); + assert.match(deviceApi, /people/); + assert.doesNotMatch(packageJson, /react-loading-skeleton/); + assert.match(readme, /Docker Compose/); + + await assert.rejects(access(new URL("../app/_sites-preview", import.meta.url))); + await access(new URL("../drizzle/", import.meta.url)); +}); diff --git a/management-console/tsconfig.json b/management-console/tsconfig.json new file mode 100644 index 0000000..3a13f90 --- /dev/null +++ b/management-console/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts", + "**/*.mts" + ], + "exclude": ["node_modules"] +} diff --git a/management-console/vite.config.ts b/management-console/vite.config.ts new file mode 100644 index 0000000..ae08f93 --- /dev/null +++ b/management-console/vite.config.ts @@ -0,0 +1,59 @@ +import vinext from "vinext"; +import { defineConfig } from "vite"; +import hostingConfig from "./.openai/hosting.json"; +import { sites } from "./build/sites-vite-plugin"; + +const SITE_CREATOR_PLACEHOLDER_DATABASE_ID = + "00000000-0000-4000-8000-000000000000"; + +const { d1, r2 } = hostingConfig; + +// macOS Seatbelt blocks FSEvents, so Codex previews need polling for HMR. +const isCodexSeatbeltSandbox = process.env.CODEX_SANDBOX === "seatbelt"; + +const localBindingConfig = { + main: "./worker/index.ts", + compatibility_flags: ["nodejs_compat"], + d1_databases: d1 + ? [ + { + binding: d1, + database_name: "site-creator-d1", + database_id: SITE_CREATOR_PLACEHOLDER_DATABASE_ID, + }, + ] + : [], + r2_buckets: r2 + ? [ + { + binding: r2, + bucket_name: "site-creator-r2", + }, + ] + : [], +}; + +export default defineConfig(async () => { + // Keep Wrangler and Miniflare state project-local. These are non-secret tool + // settings; application environment belongs in ignored `.env*` files. + process.env.WRANGLER_WRITE_LOGS ??= "false"; + process.env.WRANGLER_LOG_PATH ??= ".wrangler/logs"; + process.env.MINIFLARE_REGISTRY_PATH ??= ".wrangler/registry"; + + // Wrangler snapshots its log path while the Cloudflare plugin is imported. + const { cloudflare } = await import("@cloudflare/vite-plugin"); + + return { + server: isCodexSeatbeltSandbox + ? { watch: { useFsEvents: false, usePolling: true } } + : undefined, + plugins: [ + vinext(), + sites(), + cloudflare({ + viteEnvironment: { name: "rsc", childEnvironments: ["ssr"] }, + config: localBindingConfig, + }), + ], + }; +}); diff --git a/management-console/worker/index.ts b/management-console/worker/index.ts new file mode 100644 index 0000000..b3cd137 --- /dev/null +++ b/management-console/worker/index.ts @@ -0,0 +1,47 @@ +/** Cloudflare Worker entry point for the vinext-starter template. */ +import { handleImageOptimization, DEFAULT_DEVICE_SIZES, DEFAULT_IMAGE_SIZES } from "vinext/server/image-optimization"; +import handler from "vinext/server/app-router-entry"; + +interface Env { + ASSETS: Fetcher; + DB: D1Database; + IMAGES: { + input(stream: ReadableStream): { + transform(options: Record): { + output(options: { format: string; quality: number }): Promise<{ response(): Response }>; + }; + }; + }; +} + +interface ExecutionContext { + waitUntil(promise: Promise): void; + passThroughOnException(): void; +} + +// Image security config. SVG sources with .svg extension auto-skip the +// optimization endpoint on the client side (served directly, no proxy). +// To route SVGs through the optimizer (with security headers), set +// dangerouslyAllowSVG: true in next.config.js and uncomment below: +// const imageConfig: ImageConfig = { dangerouslyAllowSVG: true }; + +const worker = { + async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { + const url = new URL(request.url); + + if (url.pathname === "/_vinext/image") { + const allowedWidths = [...DEFAULT_DEVICE_SIZES, ...DEFAULT_IMAGE_SIZES]; + return handleImageOptimization(request, { + fetchAsset: (path) => env.ASSETS.fetch(new Request(new URL(path, request.url))), + transformImage: async (body, { width, format, quality }) => { + const result = await env.IMAGES.input(body).transform(width > 0 ? { width } : {}).output({ format, quality }); + return result.response(); + }, + }, allowedWidths); + } + + return handler.fetch(request, env, ctx); + }, +}; + +export default worker; diff --git a/model-parts/face_recognizer.csta.part-aa b/model-parts/face_recognizer.csta.part-aa new file mode 100644 index 0000000..e26e514 Binary files /dev/null and b/model-parts/face_recognizer.csta.part-aa differ diff --git a/model-parts/face_recognizer.csta.part-ab b/model-parts/face_recognizer.csta.part-ab new file mode 100644 index 0000000..82ff0e3 Binary files /dev/null and b/model-parts/face_recognizer.csta.part-ab differ diff --git a/model-parts/face_recognizer.csta.part-ac b/model-parts/face_recognizer.csta.part-ac new file mode 100644 index 0000000..0df1157 Binary files /dev/null and b/model-parts/face_recognizer.csta.part-ac differ diff --git a/model-parts/face_recognizer.csta.part-ad b/model-parts/face_recognizer.csta.part-ad new file mode 100644 index 0000000..7b8ee9a Binary files /dev/null and b/model-parts/face_recognizer.csta.part-ad differ diff --git a/scripts/restore-face-models.sh b/scripts/restore-face-models.sh new file mode 100755 index 0000000..5e66512 --- /dev/null +++ b/scripts/restore-face-models.sh @@ -0,0 +1,51 @@ +#!/bin/sh +# 将 Git 中的小于 50MB 分片恢复为 SeetaFace6 识别模型。 +set -eu + +PROJECT_DIR=$(CDPATH= cd -- "$(dirname "$0")/.." && pwd) +MODEL_FILE="$PROJECT_DIR/app/src/main/assets/face_models/face_recognizer.csta" +PART_DIR="$PROJECT_DIR/model-parts" +PART_PATTERN="$PART_DIR/face_recognizer.csta.part-" +EXPECTED_SHA256="3cefce29d2ceb6fd95153d67c11f24ce08746e1877478bacaa5ae5d333a0a670" + +sha256_file() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | awk '{print $1}' + elif command -v shasum >/dev/null 2>&1; then + shasum -a 256 "$1" | awk '{print $1}' + else + echo "错误:系统缺少 sha256sum 或 shasum,无法校验模型。" >&2 + return 1 + fi +} + +if [ -f "$MODEL_FILE" ]; then + CURRENT_SHA256=$(sha256_file "$MODEL_FILE") + if [ "$CURRENT_SHA256" = "$EXPECTED_SHA256" ]; then + echo "人脸识别模型已存在且校验通过。" + exit 0 + fi + echo "检测到损坏或版本不匹配的人脸识别模型,将从分片重新恢复。" +fi + +if ! ls "${PART_PATTERN}"* >/dev/null 2>&1; then + echo "错误:未找到人脸识别模型分片:${PART_PATTERN}*" >&2 + exit 1 +fi + +mkdir -p "$(dirname "$MODEL_FILE")" +TEMP_FILE=$(mktemp "${MODEL_FILE}.tmp.XXXXXX") +trap 'rm -f "$TEMP_FILE"' EXIT HUP INT TERM + +cat "${PART_PATTERN}"* > "$TEMP_FILE" +ACTUAL_SHA256=$(sha256_file "$TEMP_FILE") +if [ "$ACTUAL_SHA256" != "$EXPECTED_SHA256" ]; then + echo "错误:模型分片合并后的 SHA-256 校验失败。" >&2 + echo "期望:$EXPECTED_SHA256" >&2 + echo "实际:$ACTUAL_SHA256" >&2 + exit 1 +fi + +mv "$TEMP_FILE" "$MODEL_FILE" +trap - EXIT HUP INT TERM +echo "人脸识别模型恢复完成:$MODEL_FILE" diff --git a/tests/browser/harness.html b/tests/browser/harness.html new file mode 100644 index 0000000..2446c87 --- /dev/null +++ b/tests/browser/harness.html @@ -0,0 +1,147 @@ + + + + + + 星元智灵异常场景测试 + + + +
+
+ + + + +