commit 5ead239b7141f17c0cf0bdbe11c76dd008fff900 Author: choibk Date: Thu Jan 22 23:42:55 2026 +0900 Initial diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..62ac6aa --- /dev/null +++ b/.gitignore @@ -0,0 +1,67 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +# Dependencies +node_modules +.pnp +.pnp.js + +# Testing +coverage +.nyc_output + +# Production & Build +dist +dist-ssr +build +out + +# Environment Variables (CRITICAL) +.env +.env.local +.env.development.local +.env.test.local +.env.production.local +.env.production + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# OS Generated +Thumbs.db +ehthumbs.db +Desktop.ini + +# Project Specific - Server +server/uploads/* +!server/uploads/.gitkeep +server/server.zip +server/public_key.pem +server/private_key.pem + +# Project Specific - Camera/Stream +*.ts +!src/**/*.ts +!src/**/*.tsx +!vite.config.ts +!server/**/*.ts +# Exclude recorded streams if any are saved locally in dev +stream_recordings/ + +# Agent Artifacts +.gemini/ +.agent/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..d2e7761 --- /dev/null +++ b/README.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/docs/CCTV_MODULE_GUIDE.md b/docs/CCTV_MODULE_GUIDE.md new file mode 100644 index 0000000..80993c2 --- /dev/null +++ b/docs/CCTV_MODULE_GUIDE.md @@ -0,0 +1,25 @@ +# CCTV 모바일 영상 및 카메라 설정 가이드 + +본 문서는 SmartAsset CCTV 모니터링 모듈의 고급 설정 기능을 설명합니다. + +## 1. 전송 방식 (Transport Mode) +오래된 카메라나 네트워크 환경에 따라 RTSP 데이터 전송 방식을 선택할 수 있습니다. +- **TCP (권장)**: 데이터 손실 없이 안정적입니다. 기본값으로 설정되어 있습니다. +- **UDP**: 속도는 빠를 수 있으나, 패킷 손실 시 화면 깨짐이나 딜레이가 누적될 수 있습니다. +- **Auto**: 연결 시 서버가 자동으로 최적의 방식을 협상합니다. + +## 2. 스트림 화질 (Quality Settings) +네트워크 대역폭과 서버 부하를 고려하여 품질 수준을 조정할 수 있습니다. +- **Low (640px / 800kbps)**: 가장 가볍고 빠릅니다. 다중 채널 동시 감시 시 권장합니다. +- **Medium (1280px / 1500kbps)**: 밸런스가 좋은 HD급 화질입니다. +- **Original (~6000kbps)**: 카메라 원본 화질입니다. 고성능 PC 환경에서 권장합니다. + +## 3. 특수문자 처리 (RTSP Encoding) +카메라의 ID/PW에 특수문자(예: `#`, `^`, `&`)가 포함되어 있어 접속되지 않는 경우, **URL 인코딩** 옵션을 활성화하십시오. + +## 4. 카메라 위치 관리 (Drag-and-Drop) +- **관리자 전용**: 관리자 권한을 가진 사용자만 카메라의 순서를 드래그 앤 드롭으로 변경할 수 있습니다. +- **자동 저장**: 변경된 순서는 데이터베이스에 즉시 저장되어 모든 사용자의 다음 접속 시 유지됩니다. + +--- +마지막 업데이트: 2026-01-22 diff --git a/docs/DEPLOYMENT_GUIDE.md b/docs/DEPLOYMENT_GUIDE.md new file mode 100644 index 0000000..e85d901 --- /dev/null +++ b/docs/DEPLOYMENT_GUIDE.md @@ -0,0 +1,59 @@ +# 시스템 배포 가이드 (Deployment Guide) + +본 문서는 빌드된 스마트어셋(SmartAsset) 솔루션을 실서버(Synology NAS 등)에 배포할 때 필요한 절차와 주의사항을 설명합니다. + +## 1. 배포 대상 폴더 및 파일 +서버에 업로드해야 하는 핵심 구성 요소는 다음과 같습니다. + +### 1.1. `dist/` 폴더 (프론트엔드) +* **생성 방법**: `npm run build` 명령을 통해 생성됩니다. +* **역할**: 사용자 브라우저에서 실행되는 정적 웹 파일(JS, CSS, HTML)입니다. +* **위치**: 업로드 시 `server/` 폴더와 동일한 레벨 혹은 서버 설정에 맞게 배치합니다. + +### 1.2. `server/` 폴더 (백엔드) +* 프로젝트의 API 및 세션, 라이선스 검증을 담당하는 핵심 로직입니다. +* **주의**: `node_modules`는 서버에서 직접 `npm install`을 실행하여 생성하는 것을 권장합니다. + +--- + +## 2. 보안 및 라이선스 필수 파일 (중요) +배포 시 다음 파일들의 유무와 내용을 반드시 점검해야 합니다. + +### 2.1. `server/.env` (환경 변수) +* **설정**: DB 접속 정보(Host, User, Password)와 포트 번호가 포함되어 있습니다. +* **주의**: 배포 환경에 맞는 DB 정보를 정확히 입력해야 서버가 시작됩니다. + +### 2.2. `server/public_key.pem` (RSA 공개키) +* **역할**: 발급된 라이선스 키가 진짜인지 검증하는 데 사용됩니다. +* **필수**: 이 파일이 없으면 웹에서 라이선스 활성화 및 로그인이 불가능할 수 있습니다. +* **보안**: 공개되어도 무방한 파일입니다. (이 파일만으로는 라이선스 생성 불가) + +--- + +## 3. 배포 시 제외해야 할 파일 (보안 주의) +다음 파일들은 **절대로 고객사 서버에 업로드하지 마십시오.** + +1. **`tools/` 폴더 전체**: 라이선스 발급 도구가 들어 있어 고객이 직접 키를 생성할 위험이 있습니다. +2. **`tools/keys/private_key.pem` (개인키)**: 라이선스를 생성할 수 있는 마스터키입니다. 유출 시 모든 보안이 무너집니다. + +## 4. 시놀로지 역방향 프록시 설정 (CCTV 필수) +도메인(`https://demo.sokuree.com`)을 통해 접속할 경우, **WebSocket** 통신이 허용되도록 설정해야 CCTV 영상이 출력됩니다. + +1. **제어판** > **로그인 포털** > **고급** > **역방향 프록시** 이동. +2. 해당 도메인 설정 선택 후 **[편집]** 클릭. +3. **[사용자 정의 헤더]** 탭으로 이동. +4. **[생성]** 버튼 옆의 드롭다운(화살표) 클릭 > **WebSocket** 선택. +5. `Upgrade`, `Connection` 헤더가 자동으로 추가된 것을 확인하고 **[저장]**을 클릭합니다. + * *이 설정이 없으면 CCTV 화면이 검은색으로 나오거나 연결 오류가 발생합니다.* + +--- + +## 5. 서버 실행 절차 (PM2 기준) +업로드가 완료된 후 서버 터미널(SSH)에서 다음 명령을 실행합니다. + +1. **의존성 설치**: `cd server` 이동 후 `npm install` +2. **서비스 시작**: `pm2 start index.js --name "smartasset"` +3. **상태 확인**: `pm2 status`를 통해 서버가 `online` 상태인지 확인합니다. + +--- +마지막 업데이트: 2026-01-22 diff --git a/docs/IMPLEMENTATION_DETAILS.md b/docs/IMPLEMENTATION_DETAILS.md new file mode 100644 index 0000000..4d149ce --- /dev/null +++ b/docs/IMPLEMENTATION_DETAILS.md @@ -0,0 +1,77 @@ +# 구현 계획 - 모듈(라이선스) 관리 + +이 시스템의 목표는 관리자가 "라이선스 관리" 페이지를 통해 특정 모듈(자산, 생산, CCTV)을 활성화/비활성화할 수 있도록 하는 것이며, **활성화를 위해서는 유효한 라이선스 키가 필수**입니다. + +## 라이선스 시스템 아키텍처 +**서명된 토큰(Signed Token)** 방식(JWT 또는 AES 암호화와 유사)을 사용합니다. +- **발급 (개발자)**: 생성 도구(CLI 스크립트)를 사용하여 키를 생성합니다. + - 데이터: `{ module: 'cctv', type: 'demo', created: '2024-01-01', days: 30 }` + - 출력: `U2FsdGVkX1...` (암호화된 문자열) +- **활성화 (사용자)**: 사용자가 이 문자열을 입력합니다. 서버는 비밀 키(Secret Key)로 이를 복호화하고 날짜를 검증합니다. + +## 사용자 검토 사항 +> [!IMPORTANT] +> **라이선스 관리**: 보안을 위해 숨겨진 관리자 생성기(CLI 스크립트)를 사용하여 키를 발급합니다. +> **만료 처리**: 시스템은 데이터베이스에 저장된 `expiry_date`를 확인하여 만료 여부를 판별합니다. + +## 변경 제안 사항 + +### 데이터베이스 및 서버 +#### [신규] `system_modules` 테이블 +- `code` (VARCHAR, PK): 'asset', 'production', 'monitoring' +- `is_active` (BOOLEAN) +- `license_key` (TEXT): 원본 키 문자열. +- `license_type` (VARCHAR): 'dev', 'sub', 'demo' +- `expiry_date` (DATETIME): 'dev'(영구)인 경우 NULL. + +#### [신규] `server/utils/licenseManager.js` +- `generateLicense(module, type)`: 암호화된 문자열 반환. +- `verifyLicense(key)`: 복호화하여 유효성 및 만료일 반환. + +#### [신규] `server/routes/system.js` +- `POST /activate`: 키 입력 -> 복호화 -> DB 업데이트. +- `GET /modules`: 모듈 상태 및 만료 정보 반환. + +### 프론트엔드 +#### [신규] `src/system/pages/LicensePage.tsx` +- **모듈 목록**: 활성/비활성 스위치, 만료일 표시. +- **활성화 모달**: 키 입력 필드 제공. +- **(숨김) 생성기 탭**: 보안상 이유로 삭제됨, CLI로 대체. + +#### [수정] `src/widgets/layout/MainLayout.tsx` +- 모듈 활성화 상태에 따라 사이드바 메뉴 조건부 렌더링. + +## 검증 계획 +1. **키 발급**: 생성기(CLI)를 사용하여 'Demo' 키(30일)와 'Dev' 키를 생성합니다. +2. **활성화**: Demo 키를 적용합니다. 30일 후 만료되는지 확인합니다. +3. **UI 확인**: 'CCTV' 메뉴가 사이드바에 나타나는지 확인합니다. + +#### [신규] `tools/license_manager.cjs` +- **통합 CLI**: 오프라인 키 관리 및 온라인 DB 상태 관리. +- **명령어**: + - `list`: DB의 모든 모듈 상태 조회. + - `generate `: 새로운 서명된 라이선스 키 생성. + - `decode `: 라이선스 키 내용 확인 (복호화). + - `activate `: 키를 사용하여 DB에 직접 활성화 적용. + - `delete `: DB에서 모듈 비활성화/삭제. +- **삭제됨**: 기존 `issue_license.cjs` (이 도구로 통합됨). + +## 고급 카메라 설정 구현 + +### 1. 데이터베이스 스키마 확장 +`camera_settings` 테이블에 다음 컬럼을 추가하여 설정을 저장합니다. +- `transport_mode`: ENUM('tcp', 'udp', 'auto') - RTSP 전송 프로토콜 제어. +- `rtsp_encoding`: BOOLEAN - 비밀번호 특수문자 URL 인코딩 여부. +- `quality`: ENUM('low', 'medium', 'original') - 스트리밍 해상도 및 비트레이트 제어. + +### 2. 백엔드 스트림 엔진 (`streamRelay.js`) +- **FFmpeg 튜닝**: JSMpeg(소프트웨어 디코더)의 안정성을 위해 비트레이트를 제한하고 키프레임 주기를 조정했습니다. + - `-maxrate`, `-bufsize`: 네트워크 버스트 방지. + - `-g 30`: 1초마다 키프레임 전송 (화면 깨짐 시 1초 내 자동 복구). +- **심리스 리로드 (Seamless Reload)**: + - 사용자 설정 변경(PUT) 시, 클라이언트(브라우저)와의 WebSocket 연결은 유지한 채 백그라운드의 FFmpeg 프로세스만 교체합니다. + - `stopStream` 대신 `reloadStream`을 사용하여 끊김 없는 화질 변경 경험을 제공합니다. + +### 3. 프론트엔드 UI +- **CameraModal**: 고급 설정 섹션을 추가하여 사용자가 기술적인 설정(TCP/UDP, 해상도)을 직관적으로 제어할 수 있도록 폼을 개선했습니다. + diff --git a/docs/LICENSE_MANAGER_MANUAL.md b/docs/LICENSE_MANAGER_MANUAL.md new file mode 100644 index 0000000..4bcd0e6 --- /dev/null +++ b/docs/LICENSE_MANAGER_MANUAL.md @@ -0,0 +1,79 @@ +# 라이선스 관리자 매뉴얼 (최종본) + +본 문서는 스마트어셋(SmartAsset) 시스템의 라이선스 관리 기능을 사용하는 방법을 설명합니다. 모든 라이선스 관리는 **RSA 비대칭 암호화** 방식을 따르며, `tools/license_manager.cjs` CLI 도구를 통해 수행됩니다. + +## 1. 사전 준비 및 구독자 설정 (중요) +라이선스 키를 활성화하기 전에 서버가 어떤 고객사(구독자)에게 속해 있는지 식별하기 위해 **구독자 ID**를 설정해야 합니다. + +1. **웹 관리자 페이지 접속**: 상단 메뉴의 `라이선스 관리` 섹션으로 이동합니다. +2. **서버 환경 설정**: + * 부여받은 `구독자 ID`(예: `SAMSUNG`, `TEST_SUB`)를 입력하고 **[설정 저장]**을 클릭합니다. + * **주의**: 라이선스 키 발급 시 사용한 구독자 ID와 서버에 설정된 ID가 일치해야만 활성화가 가능합니다. + +--- + +## 2. 기본 명령어 (CLI 사용법) +모든 명령은 프로젝트 루트 경로에서 `node`를 사용하여 실행합니다. + +### 2.1. 라이선스 발급 (`generate`) - 제공자 전용 +새로운 RSA 서명 방식의 라이선스 키를 생성합니다. 이 작업에는 `tools/keys/private_key.pem`이 필요합니다. + +```bash +# 기본 발급 (30일 데모용) +node tools/license_manager.cjs generate monitoring demo --subscriber TEST_SUB + +# 발급 및 즉시 활성화 (로컬 테스트용) +node tools/license_manager.cjs generate asset sub --subscriber SAMSUNG --activate + +# 만기일 직접 지정 발급 +node tools/license_manager.cjs generate production sub --subscriber SAMSUNG --expiry 2026-12-31 + +# 시한부 발급 (지정된 일수 내에 활성화하지 않으면 만료됨) +node tools/license_manager.cjs generate monitoring sub --subscriber TEST_SUB --activation-window 3 +``` +* **모듈**: `asset`, `production`, `monitoring` +* **유형**: `dev` (무제한), `sub` (구독형), `demo` (30일 체험) + +### 2.2. 라이선스 검증 및 해독 (`decode`) +키를 활성화하기 전에 내용을 검증하거나, 서명 변조 여부를 확인합니다. + +```bash +node tools/license_manager.cjs decode <라이선스_키_문자열> +``` +* **Valid Signature : YES**가 나오면 정품 키이며, **NO**가 나오면 변조되었거나 잘못된 키입니다. + +### 2.3. 현황 조회 및 상세 정보 (`list`, `show`) +```bash +# 전체 모듈 활성화 상태 및 최근 발급 이력 조회 +node tools/license_manager.cjs list + +# 특정 발급 ID나 모듈의 상세 정보 확인 +node tools/license_manager.cjs show 5 +node tools/license_manager.cjs show monitoring +``` + +### 2.4. 수동 활성화 및 삭제 (`activate`, `delete`) +```bash +# 발급받은 키를 서버에 적용 (웹 UI에서도 가능) +node tools/license_manager.cjs activate <라이선스_키_문자열> + +# 특정 모듈 비활성화 (기존 키는 삭제되지 않고 이력으로 이동) +node tools/license_manager.cjs delete asset +``` + +--- + +## 3. 보안 및 배포 가이드 +보안 강화를 위해 반드시 다음 사항을 준수하십시오. + +### 3.1. 개인키와 공개키의 분리 +* **private_key.pem (제공자)**: 라이선스를 만드는 "인감"입니다. **절대로 고객사에 배포하지 마십시오.** `tools/` 폴더 자체를 배포에서 제외하는 것을 권장합니다. +* **public_key.pem (서버)**: 라이선스가 진짜인지 확인하는 "인감 증명"입니다. 서버의 `server/public_key.pem` 경로에 반드시 위치해야 합니다. + +### 3.2. 영구 보관 및 이력 +* 새로운 키로 교체될 때마다 기존 키 정보는 데이터베이스의 `license_history` 테이블에 자동으로 보관됩니다. (웹 UI에서 '변경 이력' 버튼으로 확인 가능) + +--- +마지막 업데이트: 2026-01-22 (RSA 비대칭 암호화 및 구독자 기반 관리 적용) + + diff --git a/docs/LICENSE_SERVER_SETUP.md b/docs/LICENSE_SERVER_SETUP.md new file mode 100644 index 0000000..19138e1 --- /dev/null +++ b/docs/LICENSE_SERVER_SETUP.md @@ -0,0 +1,47 @@ +# 라이선스 중앙 인증 서버 설정 가이드 + +본 시스템은 고객 서버에서 라이선스 활성화 시, 중앙 관리 서버(Developer's DB)에 접속하여 키의 유효성을 2차 검증하는 기능을 제공합니다. 이를 위해 중앙 DB 서버에 테이블을 생성하고, 고객 서버의 설정 파일(`.env`)에 접속 정보를 입력해야 합니다. + +## 1. 중앙 DB 테이블 생성 (Developer Server) + +중앙 라이선스 DB(MySQL)에 접속하여 아래 SQL을 실행하여 `issued_licenses` 테이블을 생성하십시오. +이 테이블은 생성된 모든 라이선스 키를 저장합니다. + +```sql +CREATE TABLE IF NOT EXISTS issued_licenses ( + id INT AUTO_INCREMENT PRIMARY KEY, + license_key TEXT NOT NULL, + module_code VARCHAR(50) NOT NULL, + license_type VARCHAR(20) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + used_at TIMESTAMP NULL, + is_revoked BOOLEAN DEFAULT FALSE, + INDEX idx_license_key (license_key(255)) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +``` + +## 2. 고객 서버 설정 (Client Server) + +고객사에 배포되는 서버의 `.env` 파일에 아래와 같이 중앙 라이선스 DB 접속 정보를 추가해야 합니다. 이 정보가 없으면 로컬 검증만 수행합니다. + +**주의**: 고객 서버에서 외부(중앙 DB)로 접속이 가능하도록 방화벽 설정(3306 포트 등)이 되어야 합니다. + +```env +# Central License Verification DB +LICENSE_DB_HOST=your-central-db-ip.com +LICENSE_DB_PORT=3306 +LICENSE_DB_USER=license_readonly_user +LICENSE_DB_PASSWORD=your_secure_password +LICENSE_DB_NAME=your_license_db +``` + +> **보안 권장사항**: `LICENSE_DB_USER`는 `issued_licenses` 테이블에 대한 **SELECT 권한만** 부여된 계정을 사용하십시오. + +## 3. 작동 원리 + +1. **키 생성**: 개발자가 `tools/license_manager.cjs generate` 명령을 실행하면, 로컬 키 생성과 동시에 중앙 DB(`issued_licenses`)에 키가 저장됩니다. (개발자 PC에도 `.env` 설정 필요) +2. **활성화 시도**: 고객이 라이선스 키를 입력하면, 서버는 먼저 로컬에서 서명을 검증합니다. +3. **원격 검증**: 로컬 검증이 통과되면, 서버는 `.env`에 설정된 중앙 DB에 잠시 접속하여 해당 키가 존재하는지 확인합니다. +4. **결과 처리**: 중앙 DB에 키가 존재하면 활성화가 완료되고, DB 연결은 즉시 해제됩니다. + +이 방식을 통해 임의로 키를 생성해내더라도 중앙 DB에 등록되지 않은 키는 사용할 수 없게 됩니다. diff --git a/docs/task.md b/docs/task.md new file mode 100644 index 0000000..c3d0972 --- /dev/null +++ b/docs/task.md @@ -0,0 +1,36 @@ +# SmartAsset Project - Task & Feature Roadmap + +## 1. Core Platform & Security +- [x] **Project Initialization**: Created Vite + React + TypeScript base. +- [x] **Design System**: Implemented HSL-based vanilla CSS tokens and main layout. +- [x] **Authenticated Session**: Node.js session-based authentication (HttpOnly Cookies). +- [x] **CSRF Protection**: Implemented double-submit cookie pattern for all state-changing requests. +- [x] **RBAC (Role Based Access Control)**: Restricted admin-only APIs (User/License management). +- [x] **Production Ready**: Configured PM2, static file serving for Synology NAS. +- [x] **Production Stability**: Resolved persistent `EADDRINUSE` (Port 3001) conflict by migrating backend to Port 3005. + +## 2. License Management (Security Upgraded) +- [x] **Subscriber Config**: Added Server-side Subscriber ID verification. +- [x] **RSA Asymmetric Encryption**: Replaced AES with RSA 2048-bit digital signatures. + - [x] Private Key for Provider (Signing). + - [x] Public Key for Server (Verification). +- [x] **License CLI Tool**: + - `list`: Show module status and issued history. + - `generate`: Sign keys with optional expiry and window. + - `decode`: Verify signature and inspect payload. + - `activate`: Replaces active license and archives history. +- [x] **License History**: Automatic archiving of old/replaced keys in DB. +- [x] **Manuals**: Comprehensive guides for license issuance, server setup, and deployment processes. + +## 3. CCTV Monitoring (Module) +- [x] **RTSP Relay**: WebSocket-based RTSP streaming using JSMpeg. +- [x] **Quality Control**: User-selectable resolution and bitrate levels. +- [x] **Connectivity Fixes**: RTSP password encoding and Transport mode (TCP/UDP) support. +- [x] **Synology Compatibility**: Implemented auto-detection for SynoCommunity FFmpeg (v6/v7) to support RTSP protocols. (Note: Local Windows env may block FFmpeg network access). +- [x] **Layout Persistence**: Drag-and-drop camera reordering saved to DB. +- [x] **Admin Polish**: Restricted camera edit/delete to Admin role only. + +## 4. UI/UX Refinement +- [x] **Lint/Type Fixes**: Resolved all TypeScript warnings and React usage errors. +- [x] **Clean Interface**: Glassmorphism elements and premium dark/light mode support. +- [ ] **Next Steps**: Asset maintenance history module and inventory management. diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..5e6b472 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/index.html b/index.html new file mode 100644 index 0000000..bf078b8 --- /dev/null +++ b/index.html @@ -0,0 +1,17 @@ + + + + + + + + SmartAsset - 통합 자산관리 플랫폼 + + + + +
+ + + + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..468993b --- /dev/null +++ b/package-lock.json @@ -0,0 +1,5650 @@ +{ + "name": "temp_app", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "temp_app", + "version": "0.0.0", + "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", + "@types/file-saver": "^2.0.7", + "@types/react-datepicker": "^6.2.0", + "axios": "^1.13.2", + "exceljs": "^4.4.0", + "file-saver": "^2.0.5", + "lucide-react": "^0.562.0", + "react": "^19.2.0", + "react-datepicker": "^9.1.0", + "react-dom": "^19.2.0", + "react-router-dom": "^7.12.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "autoprefixer": "^10.4.23", + "dotenv": "^17.2.3", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.17", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.4", + "vite": "^7.2.4" + } + }, + "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.28.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", + "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", + "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.28.6", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz", + "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", + "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6", + "@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.28.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz", + "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6", + "@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-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.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.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", + "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.6" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.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.28.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz", + "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", + "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", + "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/@dnd-kit/accessibility": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", + "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.3.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "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.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "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.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "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.2", + "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/eslintrc/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/@eslint/js": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "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/@fast-csv/format": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-4.3.5.tgz", + "integrity": "sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==", + "license": "MIT", + "dependencies": { + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.isboolean": "^3.0.3", + "lodash.isequal": "^4.5.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0" + } + }, + "node_modules/@fast-csv/format/node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", + "license": "MIT" + }, + "node_modules/@fast-csv/parse": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@fast-csv/parse/-/parse-4.3.6.tgz", + "integrity": "sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==", + "license": "MIT", + "dependencies": { + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.groupby": "^4.6.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0", + "lodash.isundefined": "^3.0.1", + "lodash.uniq": "^4.5.0" + } + }, + "node_modules/@fast-csv/parse/node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", + "license": "MIT" + }, + "node_modules/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react": { + "version": "0.26.28", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz", + "integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.1.2", + "@floating-ui/utils": "^0.2.8", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", + "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.4" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.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/@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/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/@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/@rolldown/pluginutils": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", + "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.2.tgz", + "integrity": "sha512-21J6xzayjy3O6NdnlO6aXi/urvSRjm6nCI6+nF6ra2YofKruGixN9kfT+dt55HVNwfDmpDHJcaS3JuP/boNnlA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.2.tgz", + "integrity": "sha512-eXBg7ibkNUZ+sTwbFiDKou0BAckeV6kIigK7y5Ko4mB/5A1KLhuzEKovsmfvsL8mQorkoincMFGnQuIT92SKqA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.2.tgz", + "integrity": "sha512-UCbaTklREjrc5U47ypLulAgg4njaqfOVLU18VrCrI+6E5MQjuG0lSWaqLlAJwsD7NpFV249XgB0Bi37Zh5Sz4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.2.tgz", + "integrity": "sha512-dP67MA0cCMHFT2g5XyjtpVOtp7y4UyUxN3dhLdt11at5cPKnSm4lY+EhwNvDXIMzAMIo2KU+mc9wxaAQJTn7sQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.2.tgz", + "integrity": "sha512-WDUPLUwfYV9G1yxNRJdXcvISW15mpvod1Wv3ok+Ws93w1HjIVmCIFxsG2DquO+3usMNCpJQ0wqO+3GhFdl6Fow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.2.tgz", + "integrity": "sha512-Ng95wtHVEulRwn7R0tMrlUuiLVL/HXA8Lt/MYVpy88+s5ikpntzZba1qEulTuPnPIZuOPcW9wNEiqvZxZmgmqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.2.tgz", + "integrity": "sha512-AEXMESUDWWGqD6LwO/HkqCZgUE1VCJ1OhbvYGsfqX2Y6w5quSXuyoy/Fg3nRqiwro+cJYFxiw5v4kB2ZDLhxrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.2.tgz", + "integrity": "sha512-ZV7EljjBDwBBBSv570VWj0hiNTdHt9uGznDtznBB4Caj3ch5rgD4I2K1GQrtbvJ/QiB+663lLgOdcADMNVC29Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.2.tgz", + "integrity": "sha512-uvjwc8NtQVPAJtq4Tt7Q49FOodjfbf6NpqXyW/rjXoV+iZ3EJAHLNAnKT5UJBc6ffQVgmXTUL2ifYiLABlGFqA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.2.tgz", + "integrity": "sha512-s3KoWVNnye9mm/2WpOZ3JeUiediUVw6AvY/H7jNA6qgKA2V2aM25lMkVarTDfiicn/DLq3O0a81jncXszoyCFA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.2.tgz", + "integrity": "sha512-gi21faacK+J8aVSyAUptML9VQN26JRxe484IbF+h3hpG+sNVoMXPduhREz2CcYr5my0NE3MjVvQ5bMKX71pfVA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.2.tgz", + "integrity": "sha512-qSlWiXnVaS/ceqXNfnoFZh4IiCA0EwvCivivTGbEu1qv2o+WTHpn1zNmCTAoOG5QaVr2/yhCoLScQtc/7RxshA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.2.tgz", + "integrity": "sha512-rPyuLFNoF1B0+wolH277E780NUKf+KoEDb3OyoLbAO18BbeKi++YN6gC/zuJoPPDlQRL3fIxHxCxVEWiem2yXw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.2.tgz", + "integrity": "sha512-g+0ZLMook31iWV4PvqKU0i9E78gaZgYpSrYPed/4Bu+nGTgfOPtfs1h11tSSRPXSjC5EzLTjV/1A7L2Vr8pJoQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.2.tgz", + "integrity": "sha512-i+sGeRGsjKZcQRh3BRfpLsM3LX3bi4AoEVqmGDyc50L6KfYsN45wVCSz70iQMwPWr3E5opSiLOwsC9WB4/1pqg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.2.tgz", + "integrity": "sha512-C1vLcKc4MfFV6I0aWsC7B2Y9QcsiEcvKkfxprwkPfLaN8hQf0/fKHwSF2lcYzA9g4imqnhic729VB9Fo70HO3Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.2.tgz", + "integrity": "sha512-68gHUK/howpQjh7g7hlD9DvTTt4sNLp1Bb+Yzw2Ki0xvscm2cOdCLZNJNhd2jW8lsTPrHAHuF751BygifW4bkQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.2.tgz", + "integrity": "sha512-1e30XAuaBP1MAizaOBApsgeGZge2/Byd6wV4a8oa6jPdHELbRHBiw7wvo4dp7Ie2PE8TZT4pj9RLGZv9N4qwlw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.2.tgz", + "integrity": "sha512-4BJucJBGbuGnH6q7kpPqGJGzZnYrpAzRd60HQSt3OpX/6/YVgSsJnNzR8Ot74io50SeVT4CtCWe/RYIAymFPwA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.2.tgz", + "integrity": "sha512-cT2MmXySMo58ENv8p6/O6wI/h/gLnD3D6JoajwXFZH6X9jz4hARqUhWpGuQhOgLNXscfZYRQMJvZDtWNzMAIDw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.2.tgz", + "integrity": "sha512-sZnyUgGkuzIXaK3jNMPmUIyJrxu/PjmATQrocpGA1WbCPX8H5tfGgRSuYtqBYAvLuIGp8SPRb1O4d1Fkb5fXaQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.2.tgz", + "integrity": "sha512-sDpFbenhmWjNcEbBcoTV0PWvW5rPJFvu+P7XoTY0YLGRupgLbFY0XPfwIbJOObzO7QgkRDANh65RjhPmgSaAjQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.2.tgz", + "integrity": "sha512-GvJ03TqqaweWCigtKQVBErw2bEhu1tyfNQbarwr94wCGnczA9HF8wqEe3U/Lfu6EdeNP0p6R+APeHVwEqVxpUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.2.tgz", + "integrity": "sha512-KvXsBvp13oZz9JGe5NYS7FNizLe99Ny+W8ETsuCyjXiKdiGrcz2/J/N8qxZ/RSwivqjQguug07NLHqrIHrqfYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.2.tgz", + "integrity": "sha512-xNO+fksQhsAckRtDSPWaMeT1uIM+JrDRXlerpnWNXhn1TdB3YZ6uKBMBTKP0eX9XtYEP978hHk1f8332i2AW8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/file-saver": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.7.tgz", + "integrity": "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==", + "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/node": { + "version": "24.10.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.9.tgz", + "integrity": "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.8.tgz", + "integrity": "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==", + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-datepicker": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@types/react-datepicker/-/react-datepicker-6.2.0.tgz", + "integrity": "sha512-+JtO4Fm97WLkJTH8j8/v3Ldh7JCNRwjMYjRaKh4KHH0M3jJoXtwiD3JBCsdlg3tsFIw9eQSqyAPeVDN2H2oM9Q==", + "license": "MIT", + "dependencies": { + "@floating-ui/react": "^0.26.2", + "@types/react": "*", + "date-fns": "^3.3.1" + } + }, + "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.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.53.1.tgz", + "integrity": "sha512-cFYYFZ+oQFi6hUnBTbLRXfTJiaQtYE3t4O692agbBl+2Zy+eqSKWtPjhPXJu1G7j4RLjKgeJPDdq3EqOwmX5Ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.53.1", + "@typescript-eslint/type-utils": "8.53.1", + "@typescript-eslint/utils": "8.53.1", + "@typescript-eslint/visitor-keys": "8.53.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.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.53.1", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.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.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.53.1.tgz", + "integrity": "sha512-nm3cvFN9SqZGXjmw5bZ6cGmvJSyJPn0wU9gHAZZHDnZl2wF9PhHv78Xf06E0MaNk4zLVHL8hb2/c32XvyJOLQg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.53.1", + "@typescript-eslint/types": "8.53.1", + "@typescript-eslint/typescript-estree": "8.53.1", + "@typescript-eslint/visitor-keys": "8.53.1", + "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", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.53.1.tgz", + "integrity": "sha512-WYC4FB5Ra0xidsmlPb+1SsnaSKPmS3gsjIARwbEkHkoWloQmuzcfypljaJcR78uyLA1h8sHdWWPHSLDI+MtNog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.53.1", + "@typescript-eslint/types": "^8.53.1", + "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.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.53.1.tgz", + "integrity": "sha512-Lu23yw1uJMFY8cUeq7JlrizAgeQvWugNQzJp8C3x8Eo5Jw5Q2ykMdiiTB9vBVOOUBysMzmRRmUfwFrZuI2C4SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.53.1", + "@typescript-eslint/visitor-keys": "8.53.1" + }, + "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.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.53.1.tgz", + "integrity": "sha512-qfvLXS6F6b1y43pnf0pPbXJ+YoXIC7HKg0UGZ27uMIemKMKA6XH2DTxsEDdpdN29D+vHV07x/pnlPNVLhdhWiA==", + "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.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.53.1.tgz", + "integrity": "sha512-MOrdtNvyhy0rHyv0ENzub1d4wQYKb2NmIqG7qEqPWFW7Mpy2jzFC3pQ2yKDvirZB7jypm5uGjF2Qqs6OIqu47w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.53.1", + "@typescript-eslint/typescript-estree": "8.53.1", + "@typescript-eslint/utils": "8.53.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.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", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.53.1.tgz", + "integrity": "sha512-jr/swrr2aRmUAUjW5/zQHbMaui//vQlsZcJKijZf3M26bnmLj8LyZUpj8/Rd6uzaek06OWsqdofN/Thenm5O8A==", + "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.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.53.1.tgz", + "integrity": "sha512-RGlVipGhQAG4GxV1s34O91cxQ/vWiHJTDHbXRr0li2q/BGg3RR/7NM8QDWgkEgrwQYCvmJV9ichIwyoKCQ+DTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.53.1", + "@typescript-eslint/tsconfig-utils": "8.53.1", + "@typescript-eslint/types": "8.53.1", + "@typescript-eslint/visitor-keys": "8.53.1", + "debug": "^4.4.3", + "minimatch": "^9.0.5", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.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.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.53.1.tgz", + "integrity": "sha512-c4bMvGVWW4hv6JmDUEG7fSYlWOl3II2I4ylt0NM+seinYQlZMQIaKaXIIVJWt9Ofh6whrpM+EdDQXKXjNovvrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.53.1", + "@typescript-eslint/types": "8.53.1", + "@typescript-eslint/typescript-estree": "8.53.1" + }, + "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", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.53.1.tgz", + "integrity": "sha512-oy+wV7xDKFPRyNggmXuZQSBzvoLnpmJs+GhzRhPjrxl2b/jIlyjVokzm47CZCDUdXKr2zd7ZLodPfOBpOPyPlg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.53.1", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz", + "integrity": "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.5", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.53", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.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/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "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/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/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/archiver": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", + "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.4", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "license": "MIT", + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/archiver-utils/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/archiver-utils/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "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/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.23", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz", + "integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001760", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "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/baseline-browser-mapping": { + "version": "2.9.15", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.15.tgz", + "integrity": "sha512-kX8h7K2srmDyYnXRIppo4AH/wYgzWVCs+eKr3RusRSQ5PvRYoEFmR/I0PbdTjKFAoKqp5+kbxnNTFO9jOfSVJg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "license": "Unlicense", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", + "license": "MIT", + "dependencies": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "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.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "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", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "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": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-indexof-polyfill": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", + "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", + "engines": { + "node": ">=0.2.0" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001765", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001765.tgz", + "integrity": "sha512-LWcNtSyZrakjECqmpP4qdg0MMGdN368D7X8XvvAqOcqMv0RxnlqVKZl2V6/mBR68oYMxOZPLw/gO7DuisMHUvQ==", + "dev": true, + "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/chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", + "license": "MIT/X11", + "dependencies": { + "traverse": ">=0.3.0 <0.4" + }, + "engines": { + "node": "*" + } + }, + "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/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/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/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/compress-commons": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", + "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "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==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", + "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, + "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/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/date-fns": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/dayjs": { + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "license": "MIT" + }, + "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/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/dotenv": { + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "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==", + "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/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "license": "BSD-3-Clause", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/duplexer2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/duplexer2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/duplexer2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "dev": true, + "license": "ISC" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "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==", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "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==", + "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/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "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-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.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.2", + "@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.12.4", + "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.2", + "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-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "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" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "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/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/exceljs": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/exceljs/-/exceljs-4.4.0.tgz", + "integrity": "sha512-XctvKaEMaj1Ii9oDOqbW/6e1gXknSY4g/aLCDicOXqBE4M0nRWkUu0PTp++UPNzoFY12BNHMfs/VadKIS6llvg==", + "license": "MIT", + "dependencies": { + "archiver": "^5.0.0", + "dayjs": "^1.8.34", + "fast-csv": "^4.3.1", + "jszip": "^3.10.1", + "readable-stream": "^3.6.0", + "saxes": "^5.0.1", + "tmp": "^0.2.0", + "unzipper": "^0.10.11", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=8.3.0" + } + }, + "node_modules/fast-csv": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/fast-csv/-/fast-csv-4.3.6.tgz", + "integrity": "sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==", + "license": "MIT", + "dependencies": { + "@fast-csv/format": "4.3.5", + "@fast-csv/parse": "4.3.6" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "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.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/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/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/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/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/file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==", + "license": "MIT" + }, + "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.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "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/fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "engines": { + "node": ">=0.6" + } + }, + "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==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "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==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "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==", + "license": "ISC" + }, + "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-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "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==", + "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.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "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/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "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": "BSD-3-Clause" + }, + "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/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, + "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/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.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-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-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/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "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/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "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/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.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/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "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/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/listenercount": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", + "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==", + "license": "ISC" + }, + "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.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, + "node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", + "license": "MIT" + }, + "node_modules/lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", + "license": "MIT" + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "license": "MIT" + }, + "node_modules/lodash.groupby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", + "integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, + "node_modules/lodash.isfunction": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", + "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==", + "license": "MIT" + }, + "node_modules/lodash.isnil": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/lodash.isnil/-/lodash.isnil-4.0.0.tgz", + "integrity": "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isundefined": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz", + "integrity": "sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==", + "license": "MIT" + }, + "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/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", + "license": "MIT" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "license": "MIT" + }, + "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/lucide-react": { + "version": "0.562.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.562.0.tgz", + "integrity": "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "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/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "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==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "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/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "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/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/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "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/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": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "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/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-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "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", + "peer": true, + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "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/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "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.3", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", + "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-datepicker": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-9.1.0.tgz", + "integrity": "sha512-lOp+m5bc+ttgtB5MHEjwiVu4nlp4CvJLS/PG1OiOe5pmg9kV73pEqO8H0Geqvg2E8gjqTaL9eRhSe+ZpeKP3nA==", + "license": "MIT", + "dependencies": { + "@floating-ui/react": "^0.27.15", + "clsx": "^2.1.1", + "date-fns": "^4.1.0" + }, + "peerDependencies": { + "date-fns-tz": "^3.0.0", + "react": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "date-fns-tz": { + "optional": true + } + } + }, + "node_modules/react-datepicker/node_modules/@floating-ui/react": { + "version": "0.27.16", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.16.tgz", + "integrity": "sha512-9O8N4SeG2z++TSM8QA/KTeKFBVCNEz/AGS7gWPJf6KFRzmRWixFRnCnkPHRDwSVZW6QPDO6uT0P2SpWNKCc9/g==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.1.6", + "@floating-ui/utils": "^0.2.10", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/react-datepicker/node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.3" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.12.0.tgz", + "integrity": "sha512-kTPDYPFzDVGIIGNLS5VJykK0HfHLY5MF3b+xj0/tTyNYL1gF1qs7u67Z9jEhQk2sQ98SUaHxlG31g1JtF7IfVw==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.12.0.tgz", + "integrity": "sha512-pfO9fiBcpEfX4Tx+iTYKDtPbrSLLCbwJ5EqP+SPYQu1VYCXdy79GSj0wttR0U4cikVdlImZuEZ/9ZNCgoaxwBA==", + "license": "MIT", + "dependencies": { + "react-router": "7.12.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.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/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/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/rollup": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.2.tgz", + "integrity": "sha512-PggGy4dhwx5qaW+CKBilA/98Ql9keyfnb7lh4SR6shQ91QQQi1ORJ1v4UinkdP2i87OBs9AQFooQylcrrRfIcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.55.2", + "@rollup/rollup-android-arm64": "4.55.2", + "@rollup/rollup-darwin-arm64": "4.55.2", + "@rollup/rollup-darwin-x64": "4.55.2", + "@rollup/rollup-freebsd-arm64": "4.55.2", + "@rollup/rollup-freebsd-x64": "4.55.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.55.2", + "@rollup/rollup-linux-arm-musleabihf": "4.55.2", + "@rollup/rollup-linux-arm64-gnu": "4.55.2", + "@rollup/rollup-linux-arm64-musl": "4.55.2", + "@rollup/rollup-linux-loong64-gnu": "4.55.2", + "@rollup/rollup-linux-loong64-musl": "4.55.2", + "@rollup/rollup-linux-ppc64-gnu": "4.55.2", + "@rollup/rollup-linux-ppc64-musl": "4.55.2", + "@rollup/rollup-linux-riscv64-gnu": "4.55.2", + "@rollup/rollup-linux-riscv64-musl": "4.55.2", + "@rollup/rollup-linux-s390x-gnu": "4.55.2", + "@rollup/rollup-linux-x64-gnu": "4.55.2", + "@rollup/rollup-linux-x64-musl": "4.55.2", + "@rollup/rollup-openbsd-x64": "4.55.2", + "@rollup/rollup-openharmony-arm64": "4.55.2", + "@rollup/rollup-win32-arm64-msvc": "4.55.2", + "@rollup/rollup-win32-ia32-msvc": "4.55.2", + "@rollup/rollup-win32-x64-gnu": "4.55.2", + "@rollup/rollup-win32-x64-msvc": "4.55.2", + "fsevents": "~2.3.2" + } + }, + "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-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "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/saxes": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=10" + } + }, + "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/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-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, + "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/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==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "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/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "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/tabbable": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", + "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", + "license": "MIT" + }, + "node_modules/tailwindcss": { + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", + "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.6", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "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/traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", + "license": "MIT/X11", + "engines": { + "node": "*" + } + }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "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/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/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", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.53.1.tgz", + "integrity": "sha512-gB+EVQfP5RDElh9ittfXlhZJdjSU4jUSTyE2+ia8CYyNvet4ElfaLlAIqDvQV9JPknKx0jQH1racTYe/4LaLSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.53.1", + "@typescript-eslint/parser": "8.53.1", + "@typescript-eslint/typescript-estree": "8.53.1", + "@typescript-eslint/utils": "8.53.1" + }, + "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", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/unzipper": { + "version": "0.10.14", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz", + "integrity": "sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==", + "license": "MIT", + "dependencies": { + "big-integer": "^1.6.17", + "binary": "~0.3.0", + "bluebird": "~3.4.1", + "buffer-indexof-polyfill": "~1.0.0", + "duplexer2": "~0.1.4", + "fstream": "^1.0.12", + "graceful-fs": "^4.2.2", + "listenercount": "~1.0.1", + "readable-stream": "~2.3.6", + "setimmediate": "~1.0.4" + } + }, + "node_modules/unzipper/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/unzipper/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/unzipper/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "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/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "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", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.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 + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "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/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/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/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "license": "MIT" + }, + "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/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "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/zip-stream": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", + "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^3.0.4", + "compress-commons": "^4.1.2", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/archiver-utils": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", + "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", + "license": "MIT", + "dependencies": { + "glob": "^7.2.3", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zod": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz", + "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==", + "dev": true, + "license": "MIT", + "peer": true, + "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/package.json b/package.json new file mode 100644 index 0000000..b6fccfc --- /dev/null +++ b/package.json @@ -0,0 +1,45 @@ +{ + "name": "temp_app", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", + "@types/file-saver": "^2.0.7", + "@types/react-datepicker": "^6.2.0", + "axios": "^1.13.2", + "exceljs": "^4.4.0", + "file-saver": "^2.0.5", + "lucide-react": "^0.562.0", + "react": "^19.2.0", + "react-datepicker": "^9.1.0", + "react-dom": "^19.2.0", + "react-router-dom": "^7.12.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "autoprefixer": "^10.4.23", + "dotenv": "^17.2.3", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.17", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.4", + "vite": "^7.2.4" + } +} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..d41ad63 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/public/vite.svg b/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/server/check_db.cjs b/server/check_db.cjs new file mode 100644 index 0000000..b720ca5 --- /dev/null +++ b/server/check_db.cjs @@ -0,0 +1,27 @@ +const path = require('path'); +require('dotenv').config({ path: path.resolve(__dirname, '.env') }); +const mysql = require('mysql2/promise'); + +async function check() { + console.log('Connecting to:', process.env.DB_NAME, 'on', process.env.DB_HOST); + const conn = await mysql.createConnection({ + host: process.env.DB_HOST, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME, + port: parseInt(process.env.DB_PORT || '3306') + }); + + try { + const [users] = await conn.execute('SELECT id, name FROM users'); + console.log('Users found:', users.map(u => u.id)); + + const [cameras] = await conn.execute('SELECT name FROM camera_settings'); + console.log('Cameras found:', cameras.map(c => c.name)); + } catch (e) { + console.error('Error:', e.message); + } finally { + await conn.end(); + } +} +check(); diff --git a/server/createAdmin.js b/server/createAdmin.js new file mode 100644 index 0000000..26b4b1f --- /dev/null +++ b/server/createAdmin.js @@ -0,0 +1,45 @@ +const db = require('./db'); +const crypto = require('crypto'); +require('dotenv').config(); + +const ID = 'admin'; +const PASSWORD = 'admin'; // Default password +const NAME = '시스템 관리자'; + +function hashPassword(password) { + return crypto.createHash('sha256').update(password).digest('hex'); +} + +async function createAdmin() { + console.log(`Creating/Updating admin user...`); + console.log(`ID: ${ID}`); + console.log(`Password: ${PASSWORD}`); + + try { + const hashedPassword = hashPassword(PASSWORD); + + // Check if exists + const [existing] = await db.query('SELECT * FROM users WHERE id = ?', [ID]); + + if (existing.length > 0) { + console.log('User exists using UPDATE...'); + await db.query('UPDATE users SET password = ?, name = ?, role = "admin" WHERE id = ?', [hashedPassword, NAME, ID]); + } else { + console.log('User does not exist. creating...'); + await db.query(` + INSERT INTO users (id, password, name, role, department, position) + VALUES (?, ?, ?, 'admin', 'IT팀', '관리자') + `, [ID, hashedPassword, NAME]); + } + + console.log('✅ Admin user setup complete.'); + console.log(`You can now login with ID: '${ID}' and password: '${PASSWORD}'`); + process.exit(0); + + } catch (error) { + console.error('❌ Failed to create admin:', error); + process.exit(1); + } +} + +createAdmin(); diff --git a/server/db.js b/server/db.js new file mode 100644 index 0000000..aa156f3 --- /dev/null +++ b/server/db.js @@ -0,0 +1,15 @@ +const mysql = require('mysql2'); +require('dotenv').config(); + +const pool = mysql.createPool({ + host: process.env.DB_HOST, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME, + port: process.env.DB_PORT || 3306, + waitForConnections: true, + connectionLimit: 10, + queueLimit: 0 +}); + +module.exports = pool.promise(); diff --git a/server/index.js b/server/index.js new file mode 100644 index 0000000..ea9aa72 --- /dev/null +++ b/server/index.js @@ -0,0 +1,506 @@ +const express = require('express'); +const cors = require('cors'); +require('dotenv').config(); + +const db = require('./db'); +const authRoutes = require('./routes/auth'); +const cameraRoutes = require('./modules/cctv/routes'); +const StreamRelay = require('./modules/cctv/streamRelay'); +const { csrfProtection } = require('./middleware/csrfMiddleware'); +const { isAuthenticated } = require('./middleware/authMiddleware'); + +const app = express(); +const PORT = process.env.PORT || 3005; // Changed to 3005 to avoid conflict with Synology services (3001 issue) +const path = require('path'); +const fs = require('fs'); + +// Ensure uploads directory exists +const uploadDir = path.join(__dirname, 'uploads'); +if (!fs.existsSync(uploadDir)) { + fs.mkdirSync(uploadDir); +} + +// Multer Configuration +const multer = require('multer'); +const storage = multer.diskStorage({ + destination: (req, file, cb) => { + cb(null, uploadDir); + }, + filename: (req, file, cb) => { + // Use timestamp + random number + extension to avoid encoding issues with Korean filenames + const ext = path.extname(file.originalname); + const filename = `${Date.now()}-${Math.round(Math.random() * 1000)}${ext}`; + cb(null, filename); + } +}); +const upload = multer({ storage: storage }); + +// Session Store Configuration +const session = require('express-session'); +const MySQLStore = require('express-mysql-session')(session); + +const sessionStoreOptions = { + host: process.env.DB_HOST, + port: process.env.DB_PORT || 3306, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME, + clearExpired: true, + checkExpirationInterval: 900000, // 15 min + expiration: 86400000 // 1 day +}; + +const sessionStore = new MySQLStore(sessionStoreOptions); + +// Middleware +app.use(cors({ + origin: true, // Allow all origins (or specific one) + credentials: true // Important for cookies +})); +app.use(express.json()); +app.use('/uploads', express.static(uploadDir)); + +// Session Middleware +app.use(session({ + key: 'smartasset_sid', + secret: process.env.SESSION_SECRET || 'smartasset_session_secret_key', + store: sessionStore, + resave: false, + saveUninitialized: true, // Save new sessions even if empty (helps with some client handshake issues) + cookie: { + httpOnly: true, // Prevent JS access + secure: false, // Set true if using HTTPS + maxAge: 1000 * 60 * 60 * 24, // 1 day + sameSite: 'lax' // Recommended for better CSRF protection and reliability + } +})); + +// Apply CSRF Protection +app.use(csrfProtection); + +// Request Logger +app.use((req, res, next) => { + console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`); + next(); +}); + +// Test DB Connection +db.query('SELECT 1') + .then(() => console.log('✅ Connected to Database')) + .catch(err => { + console.error('❌ Database Connection Failed:', err.message); + console.error('Hint: Check your .env DB_HOST and ensure Synology MariaDB allows remote connections.'); + }); + +// Ensure Tables Exist +const initTables = async () => { + try { + const assetTable = ` + CREATE TABLE IF NOT EXISTS assets ( + id VARCHAR(20) PRIMARY KEY, + name VARCHAR(100) NOT NULL, + category VARCHAR(50) NOT NULL, + image_url VARCHAR(255), + model_name VARCHAR(100), + serial_number VARCHAR(100), + manufacturer VARCHAR(100), + location VARCHAR(100), + purchase_date DATE, + purchase_price BIGINT, + manager VARCHAR(50), + status ENUM('active', 'maintain', 'broken', 'disposed') DEFAULT 'active', + calibration_cycle VARCHAR(50), + specs TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + `; + const maintenanceTable = ` + CREATE TABLE IF NOT EXISTS maintenance_history ( + id INT AUTO_INCREMENT PRIMARY KEY, + asset_id VARCHAR(20) NOT NULL, + maintenance_date DATE NOT NULL, + type VARCHAR(50) NOT NULL, + content TEXT, + image_url VARCHAR(255), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (asset_id) REFERENCES assets(id) ON DELETE CASCADE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + `; + await db.query(assetTable); + await db.query(maintenanceTable); + + const [usersTableExists] = await db.query("SHOW TABLES LIKE 'users'"); + if (usersTableExists.length === 0) { + const usersTableSQL = ` + CREATE TABLE IF NOT EXISTS users ( + id VARCHAR(50) PRIMARY KEY, + password VARCHAR(255) NOT NULL, + name VARCHAR(100) NOT NULL, + department VARCHAR(100), + position VARCHAR(100), + phone VARCHAR(255), + role ENUM('admin', 'user') DEFAULT 'user', + last_login TIMESTAMP NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + `; + await db.query(usersTableSQL); + console.log('✅ Users Table Created'); + + // Default Admin + const adminId = 'admin'; + const [adminExists] = await db.query('SELECT 1 FROM users WHERE id = ?', [adminId]); + if (adminExists.length === 0) { + const crypto = require('crypto'); + const hashedPass = crypto.createHash('sha256').update('admin123').digest('hex'); + await db.query( + 'INSERT INTO users (id, password, name, role, department, position) VALUES (?, ?, ?, ?, ?, ?)', + [adminId, hashedPass, '시스템 관리자', 'admin', 'IT팀', '관리자'] + ); + console.log('✅ Default Admin Created (admin / admin123)'); + } + } + + console.log('✅ Tables Initialized'); + // Create asset_manuals table + const manualTable = ` + CREATE TABLE IF NOT EXISTS asset_manuals ( + id INT AUTO_INCREMENT PRIMARY KEY, + asset_id VARCHAR(20) NOT NULL, + file_name VARCHAR(255) NOT NULL, + file_url VARCHAR(255) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (asset_id) REFERENCES assets(id) ON DELETE CASCADE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + `; + await db.query(manualTable); + + // Create camera_settings table + const cameraTable = ` + CREATE TABLE IF NOT EXISTS camera_settings ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(100) NOT NULL, + ip_address VARCHAR(100) NOT NULL, + port INT DEFAULT 554, + username VARCHAR(100), + password VARCHAR(100), + stream_path VARCHAR(200) DEFAULT '/stream1', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + `; + await db.query(cameraTable); + + // Check for 'transport_mode' and 'rtsp_encoding' columns and add if missing + const [camColumns] = await db.query("SHOW COLUMNS FROM camera_settings LIKE 'transport_mode'"); + if (camColumns.length === 0) { + await db.query("ALTER TABLE camera_settings ADD COLUMN transport_mode ENUM('tcp', 'udp', 'auto') DEFAULT 'tcp' AFTER stream_path"); + await db.query("ALTER TABLE camera_settings ADD COLUMN rtsp_encoding BOOLEAN DEFAULT FALSE AFTER transport_mode"); + await db.query("ALTER TABLE camera_settings ADD COLUMN quality ENUM('low', 'medium', 'original') DEFAULT 'low' AFTER transport_mode"); // Default to low for stability + console.log("✅ Added 'transport_mode', 'quality', and 'rtsp_encoding' columns to camera_settings"); + } else { + // Check if quality exists (for subsequent updates) + const [qualCol] = await db.query("SHOW COLUMNS FROM camera_settings LIKE 'quality'"); + if (qualCol.length === 0) { + await db.query("ALTER TABLE camera_settings ADD COLUMN quality ENUM('low', 'medium', 'original') DEFAULT 'low' AFTER transport_mode"); + console.log("✅ Added 'quality' column to camera_settings"); + } + } + + // Check for 'display_order' column + const [orderCol] = await db.query("SHOW COLUMNS FROM camera_settings LIKE 'display_order'"); + if (orderCol.length === 0) { + await db.query("ALTER TABLE camera_settings ADD COLUMN display_order INT DEFAULT 0 AFTER quality"); + console.log("✅ Added 'display_order' column to camera_settings"); + } + + // Create system_settings table (Key-Value store) + const systemSettingsTable = ` + CREATE TABLE IF NOT EXISTS system_settings ( + setting_key VARCHAR(50) PRIMARY KEY, + setting_value TEXT, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + `; + await db.query(systemSettingsTable); + + // Create system_modules table + const systemModulesTable = ` + CREATE TABLE IF NOT EXISTS system_modules ( + code VARCHAR(50) PRIMARY KEY, + name VARCHAR(100) NOT NULL, + is_active BOOLEAN DEFAULT FALSE, + license_key TEXT, + license_type VARCHAR(20), + expiry_date DATETIME, + subscriber_id VARCHAR(50), + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + `; + await db.query(systemModulesTable); + + // Create license_history table + const licenseHistoryTable = ` + CREATE TABLE IF NOT EXISTS license_history ( + id INT AUTO_INCREMENT PRIMARY KEY, + module_code VARCHAR(50) NOT NULL, + license_key TEXT, + license_type VARCHAR(20), + subscriber_id VARCHAR(50), + activated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + replaced_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + `; + await db.query(licenseHistoryTable); + + // Create issued_licenses table (for tracking generated keys) + const issuedLicensesTable = ` + CREATE TABLE IF NOT EXISTS issued_licenses ( + id INT AUTO_INCREMENT PRIMARY KEY, + module_code VARCHAR(50) NOT NULL, + license_key TEXT, + license_type VARCHAR(20), + subscriber_id VARCHAR(50), + status VARCHAR(20) DEFAULT 'ISSUED', -- ISSUED, ACTIVE, EXPIRED, REVOKED + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + activation_deadline DATETIME + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + `; + await db.query(issuedLicensesTable); + + // Check for 'subscriber_id' column in system_modules (for existing installations) + const [modColumns] = await db.query("SHOW COLUMNS FROM system_modules LIKE 'subscriber_id'"); + if (modColumns.length === 0) { + await db.query("ALTER TABLE system_modules ADD COLUMN subscriber_id VARCHAR(50) AFTER expiry_date"); + console.log("✅ Added 'subscriber_id' column to system_modules"); + } + + // Initialize default modules if empty (Disabled by default as per request behavior) + const [existingModules] = await db.query('SELECT 1 FROM system_modules LIMIT 1'); + if (existingModules.length === 0) { + const insert = `INSERT INTO system_modules (code, name, is_active, license_type) VALUES (?, ?, ?, ?)`; + await db.query(insert, ['asset', '자산 관리', true, 'dev']); + await db.query(insert, ['production', '생산 관리', false, null]); + await db.query(insert, ['monitoring', 'CCTV', false, null]); + } + + console.log('✅ Tables Initialized'); + } catch (err) { + console.error('❌ Table Initialization Failed:', err); + } +}; +initTables(); + +app.get('/api/health', (req, res) => { + res.json({ status: 'ok', version: '1.2.0', timestamp: '2026-01-22 21:18' }); +}); + +// Routes +app.use('/api', authRoutes); +app.use('/api/cameras', cameraRoutes); +app.use('/api/system', require('./routes/system')); + +// Protect following routes +app.use(['/api/assets', '/api/maintenance', '/api/manuals', '/api/upload'], isAuthenticated); + +// 0. File Upload Endpoint +app.post('/api/upload', upload.single('image'), (req, res) => { + if (!req.file) { + return res.status(400).json({ error: 'No file uploaded' }); + } + // Return the full URL or relative path + // Assuming server is accessed via same host, we just return the path + const fileUrl = `/uploads/${req.file.filename}`; + res.json({ url: fileUrl }); +}); + +// 1. Get All Assets +app.get('/api/assets', async (req, res) => { + try { + const [rows] = await db.query('SELECT * FROM assets ORDER BY created_at DESC'); + res.json(rows); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Database error' }); + } +}); + +// 2. Get Asset by ID +app.get('/api/assets/:id', async (req, res) => { + try { + const [rows] = await db.query('SELECT * FROM assets WHERE id = ?', [req.params.id]); + if (rows.length === 0) return res.status(404).json({ error: 'Asset not found' }); + res.json(rows[0]); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Database error' }); + } +}); + +// 3. Create Asset +app.post('/api/assets', async (req, res) => { + const { id, name, category, model_name, serial_number, manufacturer, location, purchase_date, manager, status, specs, purchase_price, image_url } = req.body; + + try { + const sql = ` + INSERT INTO assets (id, name, category, model_name, serial_number, manufacturer, location, purchase_date, manager, status, specs, purchase_price, image_url) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `; + await db.query(sql, [id, name, category, model_name, serial_number, manufacturer, location, purchase_date, manager, status, specs, purchase_price, image_url]); + res.status(201).json({ message: 'Asset created', id }); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Database error' }); + } +}); + +// 4. Update Asset +app.put('/api/assets/:id', async (req, res) => { + const { name, category, model_name, serial_number, manufacturer, location, purchase_date, manager, status, specs, purchase_price, image_url } = req.body; + + try { + const sql = ` + UPDATE assets + SET name=?, category=?, model_name=?, serial_number=?, manufacturer=?, location=?, purchase_date=?, manager=?, status=?, specs=?, purchase_price=?, image_url=? + WHERE id=? + `; + const [result] = await db.query(sql, [name, category, model_name, serial_number, manufacturer, location, purchase_date, manager, status, specs, purchase_price, image_url, req.params.id]); + + if (result.affectedRows === 0) return res.status(404).json({ error: 'Asset not found' }); + res.json({ message: 'Asset updated' }); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Database error' }); + } +}); + +// 5. Get All Maintenance Records for an Asset +app.get('/api/assets/:asset_id/maintenance', async (req, res) => { + try { + const [rows] = await db.query('SELECT * FROM maintenance_history WHERE asset_id = ? ORDER BY maintenance_date DESC', [req.params.asset_id]); + res.json(rows); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Database error' }); + } +}); + +// 6. Get Single Maintenance Record by ID +app.get('/api/maintenance/:id', async (req, res) => { + try { + const [rows] = await db.query('SELECT * FROM maintenance_history WHERE id = ?', [req.params.id]); + if (rows.length === 0) return res.status(404).json({ error: 'Maintenance record not found' }); + res.json(rows[0]); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Database error' }); + } +}); + +// 7. Create Maintenance Record +app.post('/api/assets/:asset_id/maintenance', async (req, res) => { + const { maintenance_date, type, content, images } = req.body; + const { asset_id } = req.params; + + try { + const sql = ` + INSERT INTO maintenance_history (asset_id, maintenance_date, type, content, images) + VALUES (?, ?, ?, ?, ?) + `; + const [result] = await db.query(sql, [asset_id, maintenance_date, type, content, JSON.stringify(images || [])]); + res.status(201).json({ message: 'Maintenance record created', id: result.insertId }); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Database error' }); + } +}); + +// 8. Update Maintenance Record +app.put('/api/maintenance/:id', async (req, res) => { + const { maintenance_date, type, content, images } = req.body; + + try { + const sql = ` + UPDATE maintenance_history + SET maintenance_date=?, type=?, content=?, images=? + WHERE id=? + `; + const [result] = await db.query(sql, [maintenance_date, type, content, JSON.stringify(images || []), req.params.id]); + + if (result.affectedRows === 0) return res.status(404).json({ error: 'Maintenance record not found' }); + res.json({ message: 'Maintenance record updated' }); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Database error' }); + } +}); + +// 9. Delete Maintenance Record +app.delete('/api/maintenance/:id', async (req, res) => { + try { + const [result] = await db.query('DELETE FROM maintenance_history WHERE id = ?', [req.params.id]); + + if (result.affectedRows === 0) return res.status(404).json({ error: 'Maintenance record not found' }); + res.json({ message: 'Maintenance record deleted' }); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Database error' }); + } +}); + +// 10. Get Manuals for an Asset +app.get('/api/assets/:asset_id/manuals', async (req, res) => { + try { + const [rows] = await db.query('SELECT * FROM asset_manuals WHERE asset_id = ? ORDER BY created_at DESC', [req.params.asset_id]); + res.json(rows); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Database error' }); + } +}); + +// 11. Add Manual to Asset +app.post('/api/assets/:asset_id/manuals', async (req, res) => { + const { file_name, file_url } = req.body; + const { asset_id } = req.params; + + try { + const sql = `INSERT INTO asset_manuals (asset_id, file_name, file_url) VALUES (?, ?, ?)`; + const [result] = await db.query(sql, [asset_id, file_name, file_url]); + res.status(201).json({ message: 'Manual added', id: result.insertId }); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Database error' }); + } +}); + +// 12. Delete Manual +app.delete('/api/manuals/:id', async (req, res) => { + try { + const [result] = await db.query('DELETE FROM asset_manuals WHERE id = ?', [req.params.id]); + if (result.affectedRows === 0) return res.status(404).json({ error: 'Manual not found' }); + res.json({ message: 'Manual deleted' }); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Database error' }); + } +}); + +// Serve Frontend Static Files (Production/Deployment) +app.use(express.static(path.join(__dirname, '../dist'))); + +// The "catchall" handler: for any request that doesn't +// match one above, send back React's index.html file. +app.get(/(.*)/, (req, res) => { + res.sendFile(path.join(__dirname, '../dist/index.html')); +}); + +const server = app.listen(PORT, () => { + console.log(`Server running on port ${PORT}`); +}); + +// Initialize Stream Relay +const streamRelay = new StreamRelay(server); +app.set('streamRelay', streamRelay); diff --git a/server/init_users.js b/server/init_users.js new file mode 100644 index 0000000..0759179 --- /dev/null +++ b/server/init_users.js @@ -0,0 +1,52 @@ +const db = require('./db'); +const crypto = require('crypto'); + +async function init() { + try { + console.log('Initializing Users Table...'); + + const createTableSQL = ` + CREATE TABLE IF NOT EXISTS users ( + id VARCHAR(50) PRIMARY KEY COMMENT '아이디', + password VARCHAR(255) NOT NULL COMMENT '비밀번호 (Hash)', + name VARCHAR(100) NOT NULL COMMENT '이름', + department VARCHAR(100) COMMENT '부서', + position VARCHAR(100) COMMENT '직위', + phone VARCHAR(255) COMMENT '핸드폰 (Encrypted)', + role ENUM('admin', 'user') DEFAULT 'user' COMMENT '권한', + last_login TIMESTAMP NULL COMMENT '마지막 로그인', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='사용자 관리'; + `; + + await db.query(createTableSQL); + console.log('✅ Users Table Created/Verified.'); + + const [rows] = await db.query('SELECT * FROM users WHERE id = ?', ['admin']); + if (rows.length === 0) { + // Create default admin + const password = 'admin123'; + // Simple hash for MVP: SHA-256. In production, use bcrypt or scrypt with salt. + const hashedPassword = crypto.createHash('sha256').update(password).digest('hex'); + + // Placeholder for encrypted phone + const encryptedPhone = 'pending_encryption'; + + await db.query( + 'INSERT INTO users (id, password, name, role, department, position, phone) VALUES (?, ?, ?, ?, ?, ?, ?)', + ['admin', hashedPassword, '시스템 관리자', 'admin', 'IT팀', '관리자', encryptedPhone] + ); + console.log('✅ Default Admin Account Created (admin / admin123)'); + } else { + console.log('ℹ️ Admin account already exists.'); + } + + process.exit(0); + } catch (err) { + console.error('❌ Initialization Failed:', err); + process.exit(1); + } +} + +init(); diff --git a/server/middleware/authMiddleware.js b/server/middleware/authMiddleware.js new file mode 100644 index 0000000..98c1436 --- /dev/null +++ b/server/middleware/authMiddleware.js @@ -0,0 +1,22 @@ +const isAuthenticated = (req, res, next) => { + if (req.session && req.session.user) { + return next(); + } + return res.status(401).json({ success: false, message: 'Unauthorized' }); +}; + +const hasRole = (...roles) => { + return (req, res, next) => { + if (!req.session || !req.session.user) { + return res.status(401).json({ success: false, message: 'Unauthorized' }); + } + + if (roles.includes(req.session.user.role)) { + return next(); + } + + return res.status(403).json({ success: false, message: 'Forbidden: Insufficient permissions' }); + }; +}; + +module.exports = { isAuthenticated, hasRole }; diff --git a/server/middleware/csrfMiddleware.js b/server/middleware/csrfMiddleware.js new file mode 100644 index 0000000..3ad103f --- /dev/null +++ b/server/middleware/csrfMiddleware.js @@ -0,0 +1,34 @@ +const crypto = require('crypto'); + +// Generate a random token +const generateToken = () => { + return crypto.randomBytes(24).toString('hex'); +}; + +// Middleware to protect against CSRF +const csrfProtection = (req, res, next) => { + // Skip for GET, HEAD, OPTIONS (safe methods) + if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) { + return next(); + } + + // Skip for Login endpoint (initial session creation) + if (req.path === '/api/login' || req.path === '/api/auth/login') { + return next(); + } + + // Get token from header + const tokenFromHeader = req.headers['x-csrf-token']; + + // Get token from session + const tokenFromSession = req.session.csrfToken; + + if (!tokenFromSession || !tokenFromHeader || tokenFromSession !== tokenFromHeader) { + console.error('CSRF mismatch:', { session: tokenFromSession, header: tokenFromHeader }); + return res.status(403).json({ success: false, message: 'Invalid CSRF Token' }); + } + + next(); +}; + +module.exports = { generateToken, csrfProtection }; diff --git a/server/modules/cctv/routes.js b/server/modules/cctv/routes.js new file mode 100644 index 0000000..9ebe66b --- /dev/null +++ b/server/modules/cctv/routes.js @@ -0,0 +1,141 @@ +const express = require('express'); +const router = express.Router(); +const db = require('../../db'); +const { isAuthenticated, hasRole } = require('../../middleware/authMiddleware'); + +// Get all cameras +router.get('/', async (req, res) => { + try { + const [rows] = await db.query('SELECT * FROM camera_settings ORDER BY display_order ASC, created_at DESC'); + res.json(rows); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Database error' }); + } +}); + +// Reorder cameras (Admin only) +router.put('/reorder', hasRole('admin'), async (req, res) => { + const { cameras } = req.body; // Array of { id, display_order } or just ordered IDs + if (!Array.isArray(cameras)) { + return res.status(400).json({ error: 'Invalid data format' }); + } + + try { + // Use a transaction for safety + await db.query('START TRANSACTION'); + + for (let i = 0; i < cameras.length; i++) { + const cam = cameras[i]; + // If receiving array of IDs, use index as order + const id = typeof cam === 'object' ? cam.id : cam; + const order = i; + + await db.query('UPDATE camera_settings SET display_order = ? WHERE id = ?', [order, id]); + } + + await db.query('COMMIT'); + res.json({ message: 'Cameras reordered' }); + } catch (err) { + await db.query('ROLLBACK'); + console.error(err); + res.status(500).json({ error: 'Database error' }); + } +}); + +// Get single camera +router.get('/:id', async (req, res) => { + try { + const [rows] = await db.query('SELECT * FROM camera_settings WHERE id = ?', [req.params.id]); + if (rows.length === 0) return res.status(404).json({ error: 'Camera not found' }); + res.json(rows[0]); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Database error' }); + } +}); + +// Add camera (Admin only) +router.post('/', hasRole('admin'), async (req, res) => { + const { name, ip_address, port, username, password, stream_path, transport_mode, rtsp_encoding, quality } = req.body; + try { + const sql = `INSERT INTO camera_settings (name, ip_address, port, username, password, stream_path, transport_mode, rtsp_encoding, quality) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`; + const [result] = await db.query(sql, [name, ip_address, port || 554, username, password, stream_path || '/stream1', transport_mode || 'tcp', rtsp_encoding || false, quality || 'low']); + res.status(201).json({ message: 'Camera added', id: result.insertId }); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Database error' }); + } +}); + +// Update camera (Admin only) +router.put('/:id', hasRole('admin'), async (req, res) => { + const { name, ip_address, port, username, password, stream_path, transport_mode, rtsp_encoding, quality } = req.body; + try { + const sql = `UPDATE camera_settings SET name=?, ip_address=?, port=?, username=?, password=?, stream_path=?, transport_mode=?, rtsp_encoding=?, quality=? WHERE id=?`; + const [result] = await db.query(sql, [name, ip_address, port, username, password, stream_path, transport_mode, rtsp_encoding, quality, req.params.id]); + if (result.affectedRows === 0) return res.status(404).json({ error: 'Camera not found' }); + + // Force stream reset (kick clients to trigger reconnect) + const streamRelay = req.app.get('streamRelay'); + if (streamRelay) { + console.log(`Settings changed for camera ${req.params.id}, resetting stream...`); + streamRelay.resetStream(req.params.id); + } + + res.json({ message: 'Camera updated' }); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Database error' }); + } +}); + +// Delete camera (Admin only) +router.delete('/:id', hasRole('admin'), async (req, res) => { + try { + const [result] = await db.query('DELETE FROM camera_settings WHERE id = ?', [req.params.id]); + if (result.affectedRows === 0) return res.status(404).json({ error: 'Camera not found' }); + + // Stop stream + const streamRelay = req.app.get('streamRelay'); + if (streamRelay) { + streamRelay.stopStream(req.params.id); + } + + res.json({ message: 'Camera deleted' }); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Database error' }); + } +}); + +const { exec } = require('child_process'); + +// ... existing routes ... + +// 7. Ping Test (Troubleshooting) +router.get('/:id/ping', isAuthenticated, async (req, res) => { + try { + const [rows] = await db.query('SELECT ip_address FROM camera_settings WHERE id = ?', [req.params.id]); + if (rows.length === 0) return res.status(404).json({ error: 'Camera not found' }); + + const ip = rows[0].ip_address; + + // Simple ping command + const platform = process.platform; + const cmd = platform === 'win32' ? `ping -n 1 ${ip}` : `ping -c 1 ${ip}`; + + exec(cmd, (error, stdout, stderr) => { + res.json({ + ip, + success: !error, + output: stdout, + error: stderr + }); + }); + } catch (err) { + res.status(500).json({ error: 'Ping failed' }); + } +}); + +module.exports = router; diff --git a/server/modules/cctv/streamRelay.js b/server/modules/cctv/streamRelay.js new file mode 100644 index 0000000..90c493d --- /dev/null +++ b/server/modules/cctv/streamRelay.js @@ -0,0 +1,248 @@ +const WebSocket = require('ws'); +const ffmpeg = require('fluent-ffmpeg'); +const ffmpegPath = require('ffmpeg-static'); +const db = require('../../db'); + +const fs = require('fs'); + +// Determine FFmpeg path: Prefer system install on Synology, fallback to static +// Determine FFmpeg path: Prioritize Community packages (RTSP support) > System (limited) > Static +let systemFfmpegPath = ffmpegPath; + +// Potential paths for fully-featured FFmpeg (SynoCommunity, etc.) +// User 'find /usr' showed it's NOT in /usr/local/bin, so we must look in package dirs. +const priorityPaths = [ + // SynoCommunity or Package Center standard paths + '/var/packages/ffmpeg/target/bin/ffmpeg', + '/var/packages/ffmpeg7/target/bin/ffmpeg', + '/var/packages/ffmpeg6/target/bin/ffmpeg', + + // Direct volume paths (common on Synology) + '/volume1/@appstore/ffmpeg/bin/ffmpeg', + '/volume1/@appstore/ffmpeg7/bin/ffmpeg', + '/volume1/@appstore/ffmpeg6/bin/ffmpeg', + + // Standard Linux paths + '/usr/local/bin/ffmpeg', + '/usr/bin/ffmpeg', + '/bin/ffmpeg' +]; + +let foundPath = false; +for (const path of priorityPaths) { + if (fs.existsSync(path)) { + systemFfmpegPath = path; + console.log(`[StreamRelay] Found FFmpeg at: ${path}`); + // If we found a path that is NOT the system default (which we know is broken), likely we are good. + // But even if it is system default, we use it if nothing else found. + if (path !== '/usr/bin/ffmpeg' && path !== '/bin/ffmpeg') { + console.log('[StreamRelay] Using generic/community FFmpeg (likely supports RTSP).'); + } else { + console.warn('[StreamRelay] WARNING: Using system FFmpeg. RTSP might fail ("Protocol not found").'); + } + foundPath = true; + break; + } +} + +if (!foundPath) { + console.warn('[StreamRelay] No FFmpeg binary found in priority paths. Falling back to ffmpeg-static or default PATH.'); +} + +ffmpeg.setFfmpegPath(systemFfmpegPath); + +class StreamRelay { + constructor(server) { + this.wss = new WebSocket.Server({ server, path: '/api/stream' }); + this.streams = new Map(); // cameraId -> { process, clients: Set } + + this.wss.on('connection', (ws, req) => { + const cameraId = this.parseCameraId(req.url); + if (!cameraId) { + ws.close(); + return; + } + + console.log(`Client connected to stream for camera ${cameraId}`); + this.addClient(cameraId, ws); + + ws.on('close', () => { + console.log(`Client disconnected from stream for camera ${cameraId}`); + this.removeClient(cameraId, ws); + }); + }); + } + + parseCameraId(url) { + // url format: /api/stream?cameraId=1 or /api/stream/1 (if path matching works that way, but ws path is fixed) + // Let's use query param: ws://host/api/stream?cameraId=1 + try { + const params = new URLSearchParams(url.split('?')[1]); + return params.get('cameraId'); + } catch (e) { + return null; + } + } + + async addClient(cameraId, ws) { + let stream = this.streams.get(cameraId); + if (!stream) { + stream = { clients: new Set(), process: null }; + this.streams.set(cameraId, stream); + await this.startFFmpeg(cameraId); + } + stream.clients.add(ws); + } + + removeClient(cameraId, ws) { + const stream = this.streams.get(cameraId); + if (stream) { + stream.clients.delete(ws); + if (stream.clients.size === 0) { + this.stopStream(cameraId); + } + } + } + + // Forcefully disconnect all clients to trigger reconnection + resetStream(cameraId) { + const stream = this.streams.get(cameraId); + if (stream) { + console.log(`Resetting stream for camera ${cameraId} (closing ${stream.clients.size} clients)...`); + // Close all clients. This will trigger 'close' event on ws, + // calling removeClient -> stopStream. + for (const client of stream.clients) { + if (client.readyState === WebSocket.OPEN) { + client.close(1000, "Stream Reset"); + } + } + // Ensure stream is stopped even if no clients existed + this.stopStream(cameraId); + } + } + + async startFFmpeg(cameraId) { + const stream = this.streams.get(cameraId); + if (!stream) return; + + try { + const [rows] = await db.query('SELECT * FROM camera_settings WHERE id = ?', [cameraId]); + if (rows.length === 0) { + console.error(`Camera ${cameraId} not found`); + return; + } + const camera = rows[0]; + + // Construct RTSP URL + let rtspUrl = 'rtsp://'; + if (camera.username && camera.password) { + const user = camera.rtsp_encoding ? encodeURIComponent(camera.username) : camera.username; + const pass = camera.rtsp_encoding ? encodeURIComponent(camera.password) : camera.password; + rtspUrl += `${user}:${pass}@`; + } + rtspUrl += `${camera.ip_address}:${camera.port || 554}${camera.stream_path || '/stream1'}`; + + console.log(`Starting FFmpeg for camera ${cameraId} (${camera.name})...`); + + const transportMode = camera.transport_mode === 'udp' ? 'udp' : 'tcp'; + + // Determine Quality Scaling + let scaleFilter = []; + let videoBitrate = '1000k'; + + const qual = camera.quality || 'low'; + + if (qual === 'low') { + scaleFilter = ['-vf scale=640:-1']; + videoBitrate = '1000k'; + } else if (qual === 'medium') { + scaleFilter = ['-vf scale=1280:-1']; + videoBitrate = '2500k'; + } else { + // Original - Restore high bitrate as requested + videoBitrate = '6000k'; + } + + // Compatibility: Synology FFmpeg might not support -rtsp_transport flag or strict ordering + // Try removing explicit transport mode if it fails, or rely on Auto. + const inputOptions = []; + if (transportMode !== 'auto') { + // We restored this because we are now ensuring a proper FFmpeg version (v6/7 or static) is used. + // REVERT: User reported local Windows issues with this flag enforced. + // Commenting out to restore local functionality. Synology should still work via Auto or default. + // inputOptions.push(`-rtsp_transport ${transportMode}`); + } + + console.log(`[StreamRelay] Using FFmpeg binary: ${systemFfmpegPath}`); + + const command = ffmpeg(rtspUrl) + .inputOptions(inputOptions) + // .inputOptions([ + // `-rtsp_transport ${transportMode}` + // ]) + .addOptions([ + '-c:v mpeg1video', // Video codec for JSMpeg + '-f mpegts', // Output format + '-codec:a mp2', // Audio codec + `-b:v ${videoBitrate}`, // Dynamic Video bitrate + `-maxrate ${videoBitrate}`, // Cap max bitrate + `-bufsize ${videoBitrate}`, + '-r 30', // FPS 30 (Restored) + '-g 30', // GOP size 30 (1 keyframe per sec) + '-bf 0', // No B-frames + ...scaleFilter + ]) + .on('start', (cmdLine) => { + console.log('FFmpeg started:', cmdLine); + }) + .on('error', (err) => { + // Only log if not killed manually (checking active stream usually tricky, but error msg usually enough) + if (!err.message.includes('SIGKILL')) { + console.error('FFmpeg error:', err.message); + // If error occurs, maybe stop stream? + // For reload, we might retry? simple stop for now. + this.stopStream(cameraId); + } + }) + .on('end', () => { + console.log('FFmpeg exited'); + // We don't indiscriminately stopStream here because 'reload' causes an exit too. + // But 'stopStream' checks if process matches? + // Let's rely on the process being replaced or nullified. + }); + + stream.process = command; + + const ffstream = command.pipe(); + + ffstream.on('data', (data) => { + stream.clients.forEach(client => { + if (client.readyState === WebSocket.OPEN) { + client.send(data); + } + }); + }); + + } catch (err) { + console.error('Failed to start stream:', err); + this.stopStream(cameraId); + } + } + + stopStream(cameraId) { + const stream = this.streams.get(cameraId); + if (stream) { + console.log(`Stopping stream for camera ${cameraId}`); + if (stream.process) { + try { + stream.process.kill('SIGKILL'); + } catch (e) { + console.error('Error killing ffmpeg:', e); + } + } + this.streams.delete(cameraId); + } + } +} + +module.exports = StreamRelay; diff --git a/server/package-lock.json b/server/package-lock.json new file mode 100644 index 0000000..6fff1f1 --- /dev/null +++ b/server/package-lock.json @@ -0,0 +1,1814 @@ +{ + "name": "server", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "server", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "cors": "^2.8.5", + "dotenv": "^17.2.3", + "express": "^5.2.1", + "express-mysql-session": "^3.0.3", + "express-session": "^1.18.2", + "ffmpeg-static": "^5.3.0", + "fluent-ffmpeg": "^2.1.3", + "multer": "^2.0.2", + "mysql2": "^3.16.1", + "ws": "^8.19.0" + }, + "devDependencies": { + "nodemon": "^3.1.11" + } + }, + "node_modules/@derhuerst/http-basic": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/@derhuerst/http-basic/-/http-basic-8.2.4.tgz", + "integrity": "sha512-F9rL9k9Xjf5blCz8HsJRO4diy111cayL2vkY2XE4r4t3n0yPXVYy3KD3nJ1qbrSn9743UWSXH4IwuCa/HWlGFw==", + "license": "MIT", + "dependencies": { + "caseless": "^0.12.0", + "concat-stream": "^2.0.0", + "http-response-object": "^3.0.1", + "parse-cache-control": "^1.0.1" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@types/node": { + "version": "10.17.60", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", + "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==", + "license": "MIT" + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/async": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", + "integrity": "sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ==" + }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "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/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "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/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==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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==", + "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==", + "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/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "license": "Apache-2.0" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "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/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dotenv": { + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "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==", + "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/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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==", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-mysql-session": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/express-mysql-session/-/express-mysql-session-3.0.3.tgz", + "integrity": "sha512-sEYrzFrOs3er+Ie/uk1dt93qz4AQ9SU1mpJJ0HPs0MJ4t4hE9AcDRNq0sZQUwy2F/SbXusBt1E5+FY6KzSqXNg==", + "license": "MIT", + "dependencies": { + "debug": "4.3.4", + "mysql2": "3.10.2" + } + }, + "node_modules/express-mysql-session/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/express-mysql-session/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/express-mysql-session/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "license": "MIT" + }, + "node_modules/express-mysql-session/node_modules/mysql2": { + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.10.2.tgz", + "integrity": "sha512-KCXPEvAkO0RcHPr362O5N8tFY2fXvbjfkPvRY/wGumh4EOemo9Hm5FjQZqv/pCmrnuxGu5OxnSENG0gTXqKMgQ==", + "license": "MIT", + "dependencies": { + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.6.3", + "long": "^5.2.1", + "lru-cache": "^8.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/express-session": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.2.tgz", + "integrity": "sha512-SZjssGQC7TzTs9rpPDuUrR23GNZ9+2+IkA/+IJWmvQilTr5OSliEHGF+D9scbIpdC6yGtTI0/VhaHoVes2AN/A==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.7", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.1.0", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express-session/node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/express-session/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express-session/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/ffmpeg-static": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/ffmpeg-static/-/ffmpeg-static-5.3.0.tgz", + "integrity": "sha512-H+K6sW6TiIX6VGend0KQwthe+kaceeH/luE8dIZyOP35ik7ahYojDuqlTV1bOrtEwl01sy2HFNGQfi5IDJvotg==", + "hasInstallScript": true, + "license": "GPL-3.0-or-later", + "dependencies": { + "@derhuerst/http-basic": "^8.2.0", + "env-paths": "^2.2.0", + "https-proxy-agent": "^5.0.0", + "progress": "^2.0.3" + }, + "engines": { + "node": ">=16" + } + }, + "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/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fluent-ffmpeg": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/fluent-ffmpeg/-/fluent-ffmpeg-2.1.3.tgz", + "integrity": "sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT", + "dependencies": { + "async": "^0.2.9", + "which": "^1.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "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==", + "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==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-response-object": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/http-response-object/-/http-response-object-3.0.2.tgz", + "integrity": "sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==", + "license": "MIT", + "dependencies": { + "@types/node": "^10.0.3" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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-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-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-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lru-cache": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-8.0.5.tgz", + "integrity": "sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==", + "license": "ISC", + "engines": { + "node": ">=16.14" + } + }, + "node_modules/lru.min": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.3.tgz", + "integrity": "sha512-Lkk/vx6ak3rYkRR0Nhu4lFUT2VDnQSxBe8Hbl7f36358p6ow8Bnvr8lrLt98H8J1aGxfhbX4Fs5tYg2+FTwr5Q==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "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==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", + "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "mkdirp": "^0.5.6", + "object-assign": "^4.1.1", + "type-is": "^1.6.18", + "xtend": "^4.0.2" + }, + "engines": { + "node": ">= 10.16.0" + } + }, + "node_modules/multer/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mysql2": { + "version": "3.16.1", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.16.1.tgz", + "integrity": "sha512-b75qsDB3ieYEzMsT1uRGsztM/sy6vWPY40uPZlVVl8eefAotFCoS7jaDB5DxDNtlW5kdVGd9jptSpkvujNxI2A==", + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.1", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.0", + "long": "^5.2.1", + "lru.min": "^1.0.0", + "named-placeholders": "^1.1.6", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", + "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", + "license": "MIT", + "dependencies": { + "lru.min": "^1.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nodemon": { + "version": "3.1.11", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz", + "integrity": "sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parse-cache-control": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz", + "integrity": "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg==" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "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/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "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==", + "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.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "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==", + "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==", + "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/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "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/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "license": "MIT", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "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/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + } + } +} diff --git a/server/package.json b/server/package.json new file mode 100644 index 0000000..e8c7510 --- /dev/null +++ b/server/package.json @@ -0,0 +1,30 @@ +{ + "name": "server", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "start": "node index.js", + "dev": "nodemon index.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "dependencies": { + "cors": "^2.8.5", + "dotenv": "^17.2.3", + "express": "^5.2.1", + "express-mysql-session": "^3.0.3", + "express-session": "^1.18.2", + "ffmpeg-static": "^5.3.0", + "fluent-ffmpeg": "^2.1.3", + "multer": "^2.0.2", + "mysql2": "^3.16.1", + "ws": "^8.19.0" + }, + "devDependencies": { + "nodemon": "^3.1.11" + } +} diff --git a/server/routes/auth.js b/server/routes/auth.js new file mode 100644 index 0000000..8a4de01 --- /dev/null +++ b/server/routes/auth.js @@ -0,0 +1,251 @@ +const express = require('express'); +const router = express.Router(); +const db = require('../db'); +const crypto = require('crypto'); +const { isAuthenticated, hasRole } = require('../middleware/authMiddleware'); +const { generateToken } = require('../middleware/csrfMiddleware'); + +// --- Crypto Utilities --- +// Use a fixed key for MVP. In production, store this securely in .env +// Key must be 32 bytes for aes-256-cbc +// 'my_super_secret_key_manage_asset' is 32 chars? +// let's use a simpler approach to ensure length on startup or fallback +const SECRET_KEY = process.env.ENCRYPTION_KEY || 'smartasset_secret_key_0123456789'; // 32 chars needed +// Ideally use a buffer from hex, but string is okay if 32 chars. +// Let's pad it to ensure stability if env is missing. +const keyBuffer = crypto.scryptSync(SECRET_KEY, 'salt', 32); + +const ALGORITHM = 'aes-256-cbc'; + +function encrypt(text) { + if (!text) return text; + const iv = crypto.randomBytes(16); + const cipher = crypto.createCipheriv(ALGORITHM, keyBuffer, iv); + let encrypted = cipher.update(text, 'utf8', 'hex'); + encrypted += cipher.final('hex'); + return iv.toString('hex') + ':' + encrypted; +} + +function decrypt(text) { + if (!text) return text; + // Check if it looks like our encrypted format (hexIV:hexContent) + if (!text.includes(':')) { + return text; // Assume plain text if no separator + } + + try { + const textParts = text.split(':'); + const ivHex = textParts.shift(); + + // IV for AES-256-CBC must be 16 bytes (32 hex characters) + if (!ivHex || ivHex.length !== 32) { + return text; // Invalid IV length, return original + } + + const iv = Buffer.from(ivHex, 'hex'); + const encryptedText = textParts.join(':'); + + const decipher = crypto.createDecipheriv(ALGORITHM, keyBuffer, iv); + let decrypted = decipher.update(encryptedText, 'hex', 'utf8'); + decrypted += decipher.final('utf8'); + return decrypted; + } catch (e) { + console.error('Decryption failed for:', text, e.message); + return text; // Return original if fail + } +} + +function hashPassword(password) { + return crypto.createHash('sha256').update(password).digest('hex'); +} + +// --- Routes --- + +// 1. Login +router.post('/login', async (req, res) => { + const { id, password } = req.body; + try { + const hashedPassword = hashPassword(password); + const [rows] = await db.query('SELECT * FROM users WHERE id = ? AND password = ?', [id, hashedPassword]); + + if (rows.length > 0) { + const user = rows[0]; + // Update last_login + await db.query('UPDATE users SET last_login = NOW() WHERE id = ?', [user.id]); + + // Remove sensitive data + delete user.password; + + // Should we decrypt phone? Maybe not needed for session, but let's decrypt just in case UI needs it + if (user.phone) user.phone = decrypt(user.phone); + + // Save user to session + req.session.user = user; + + // Generate CSRF Token + const csrfToken = generateToken(); + req.session.csrfToken = csrfToken; + + // Explicitly save session before response (optional but safer for race conditions) + req.session.save(err => { + if (err) { + console.error('Session save error:', err); + return res.status(500).json({ success: false, message: 'Session error' }); + } + res.json({ success: true, user, csrfToken }); + }); + } else { + res.status(401).json({ success: false, message: 'Invalid credentials' }); + } + } catch (err) { + console.error('Login error:', err); + res.status(500).json({ success: false, message: 'Server error' }); + } +}); + +// 1.5. Check Session (New) +router.get('/check', (req, res) => { + if (req.session.user) { + // Ensure CSRF token exists, if not generate one (edge case) + if (!req.session.csrfToken) { + req.session.csrfToken = generateToken(); + } + res.json({ + isAuthenticated: true, + user: req.session.user, + csrfToken: req.session.csrfToken + }); + } else { + res.json({ isAuthenticated: false }); + } +}); + +// 1.6. Logout (New) +router.post('/logout', (req, res) => { + req.session.destroy(err => { + if (err) { + console.error('Logout error:', err); + return res.status(500).json({ success: false, message: 'Logout failed' }); + } + res.clearCookie('smartasset_sid'); // matching key in index.js + res.json({ success: true, message: 'Logged out' }); + }); +}); + +// 2. List Users (Admin Only) +router.get('/users', isAuthenticated, hasRole('admin'), async (req, res) => { + try { + // ideally check req.user.role if we had middleware, for now assuming client logic protection + internal/local usage + const [rows] = await db.query('SELECT id, name, department, position, phone, role, last_login, created_at, updated_at FROM users ORDER BY created_at DESC'); + + const users = rows.map(u => ({ + ...u, + phone: decrypt(u.phone) // Decrypt phone for admin view + })); + + res.json(users); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Database error' }); + } +}); + +// 3. Create User +router.post('/users', isAuthenticated, hasRole('admin'), async (req, res) => { + const { id, password, name, department, position, phone, role } = req.body; + + if (!id || !password || !name) { + return res.status(400).json({ error: 'Missing required fields' }); + } + + try { + // Check if ID exists + const [existing] = await db.query('SELECT id FROM users WHERE id = ?', [id]); + if (existing.length > 0) { + return res.status(409).json({ error: 'User ID already exists' }); + } + + const hashedPassword = hashPassword(password); + const encryptedPhone = encrypt(phone); + + const sql = ` + INSERT INTO users (id, password, name, department, position, phone, role) + VALUES (?, ?, ?, ?, ?, ?, ?) + `; + + await db.query(sql, [id, hashedPassword, name, department, position, encryptedPhone, role || 'user']); + + res.status(201).json({ message: 'User created' }); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Database error' }); + } +}); + +// 4. Update User +router.put('/users/:id', isAuthenticated, hasRole('admin'), async (req, res) => { + const { password, name, department, position, phone, role } = req.body; + const userId = req.params.id; + + try { + // Fetch current to keep old values if not provided? Frontend usually sends all. + // We update everything provided. + + // Build query dynamically or just assume full update + let updates = []; + let params = []; + + if (password) { + updates.push('password = ?'); + params.push(hashPassword(password)); + } + if (name) { + updates.push('name = ?'); + params.push(name); + } + if (department !== undefined) { + updates.push('department = ?'); + params.push(department); + } + if (position !== undefined) { + updates.push('position = ?'); + params.push(position); + } + if (phone !== undefined) { + updates.push('phone = ?'); + params.push(encrypt(phone)); + } + if (role) { + updates.push('role = ?'); + params.push(role); + } + + if (updates.length === 0) return res.json({ message: 'No changes' }); + + const sql = `UPDATE users SET ${updates.join(', ')} WHERE id = ?`; + params.push(userId); + + await db.query(sql, params); + res.json({ message: 'User updated' }); + + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Database error' }); + } +}); + +// 5. Delete User +router.delete('/users/:id', isAuthenticated, hasRole('admin'), async (req, res) => { + try { + // Prevent deleting last admin? Optional. + // Prevent deleting self? + + await db.query('DELETE FROM users WHERE id = ?', [req.params.id]); + res.json({ message: 'User deleted' }); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Database error' }); + } +}); + +module.exports = router; diff --git a/server/routes/system.js b/server/routes/system.js new file mode 100644 index 0000000..0866eec --- /dev/null +++ b/server/routes/system.js @@ -0,0 +1,201 @@ +const express = require('express'); +const router = express.Router(); +const db = require('../db'); +const path = require('path'); +const fs = require('fs'); +const { isAuthenticated, hasRole } = require('../middleware/authMiddleware'); +const { generateLicense, verifyLicense } = require('../utils/licenseManager'); +const { checkRemoteKey } = require('../utils/remoteLicense'); + +// Load Public Key for Verification +const publicKeyPath = path.join(__dirname, '../public_key.pem'); +let publicKey = null; +try { + if (fs.existsSync(publicKeyPath)) { + publicKey = fs.readFileSync(publicKeyPath, 'utf8'); + } else { + console.error('WARNING: public_key.pem not found in server root. License verification will fail.'); + } +} catch (e) { + console.error('Error loading public key:', e); +} + +// 0. Server Configuration (Subscriber ID) +router.get('/config', isAuthenticated, hasRole('admin'), async (req, res) => { + try { + const [rows] = await db.query("SELECT setting_value FROM system_settings WHERE setting_key = 'subscriber_id'"); + res.json({ subscriber_id: rows.length > 0 ? rows[0].setting_value : '' }); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Database error' }); + } +}); + +router.post('/config', isAuthenticated, hasRole('admin'), async (req, res) => { + const { subscriber_id } = req.body; + if (!subscriber_id) return res.status(400).json({ error: 'Subscriber ID is required' }); + + try { + const sql = `INSERT INTO system_settings (setting_key, setting_value) VALUES ('subscriber_id', ?) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)`; + await db.query(sql, [subscriber_id]); + res.json({ message: 'Subscriber ID saved' }); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Database error' }); + } +}); + +// 1. Get All Modules Status +router.get('/modules', isAuthenticated, async (req, res) => { + try { + const [rows] = await db.query('SELECT * FROM system_modules'); + + const modules = {}; + const defaults = ['asset', 'production', 'monitoring']; + + // Get stored subscriber ID + const [subRows] = await db.query("SELECT setting_value FROM system_settings WHERE setting_key = 'subscriber_id'"); + const serverSubscriberId = subRows.length > 0 ? subRows[0].setting_value : null; + + defaults.forEach(code => { + const found = rows.find(r => r.code === code); + if (found) { + modules[code] = { + active: !!found.is_active, + type: found.license_type, + expiry: found.expiry_date, + subscriber_id: found.subscriber_id // Return who verified it + }; + } else { + modules[code] = { active: false, type: null, expiry: null, subscriber_id: null }; + } + }); + + res.json({ modules, serverSubscriberId }); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Database error' }); + } +}); + +// 2. Activate Module (Apply License) +// Only admin can manage system modules +router.post('/modules/:code/activate', isAuthenticated, hasRole('admin'), async (req, res) => { + const { code } = req.params; + let { licenseKey } = req.body; + + if (!licenseKey) { + return res.status(400).json({ error: 'License key is required' }); + } + + licenseKey = licenseKey.trim(); + + // 1. Verify Key validity + const result = verifyLicense(licenseKey, publicKey); + if (!result.isValid) { + return res.status(400).json({ error: `Invalid License: ${result.reason}` }); + } + + // 2. Check Module match + if (result.module !== code) { + return res.status(400).json({ error: `This license is for '${result.module}' module, not '${code}'` }); + } + + // 3. Check Subscriber match + try { + const [subRows] = await db.query("SELECT setting_value FROM system_settings WHERE setting_key = 'subscriber_id'"); + const serverSubscriberId = subRows.length > 0 ? subRows[0].setting_value : null; + + if (!serverSubscriberId) { + return res.status(400).json({ error: 'Server Subscriber ID is not set. Please configure it in settings first.' }); + } + + if (result.subscriberId !== serverSubscriberId) { + return res.status(403).json({ + error: `License Subscriber Mismatch. Key is for '${result.subscriberId}', but Server is '${serverSubscriberId}'.` + }); + } + + // 4. Archive Current License if exists + const [current] = await db.query('SELECT * FROM system_modules WHERE code = ?', [code]); + if (current.length > 0 && current[0].is_active && current[0].license_key) { + const old = current[0]; + const historySql = ` + INSERT INTO license_history (module_code, license_key, license_type, subscriber_id, activated_at) + VALUES (?, ?, ?, ?, NOW()) + `; + // Store "activated_at" as roughly the time it was active until now (or simply log the event time) + // Actually, let's treat "activated_at" in history as "when it was archived/replaced". + await db.query(historySql, [old.code, old.license_key, old.license_type, old.subscriber_id]); + } + + // Upsert into system_modules + const sql = ` + INSERT INTO system_modules (code, name, is_active, license_key, license_type, expiry_date, subscriber_id) + VALUES (?, ?, true, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + is_active = true, + license_key = VALUES(license_key), + license_type = VALUES(license_type), + expiry_date = VALUES(expiry_date), + subscriber_id = VALUES(subscriber_id) + `; + + // Map codes to names + const names = { + 'asset': '자산 관리', + 'production': '생산 관리', + 'monitoring': 'CCTV' + }; + + await db.query(sql, [code, names[code] || code, licenseKey, result.type, result.expiryDate, result.subscriberId]); + + // 5. Update Issued License Status (if exists in issued_licenses table) + // This ensures CLI 'list' command reflects activation even if done via UI + await db.query("UPDATE issued_licenses SET status = 'ACTIVE' WHERE license_key = ?", [licenseKey]); + + res.json({ success: true, message: 'Module activated', type: result.type, expiry: result.expiryDate }); + + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Database error' }); + } +}); + +// 3. Get Module History +router.get('/modules/:code/history', isAuthenticated, hasRole('admin'), async (req, res) => { + try { + const [rows] = await db.query('SELECT * FROM license_history WHERE module_code = ? ORDER BY id DESC LIMIT 10', [req.params.code]); + res.json(rows); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Database error' }); + } +}); + +// 4. Deactivate Module +router.post('/modules/:code/deactivate', isAuthenticated, hasRole('admin'), async (req, res) => { + const { code } = req.params; + try { + // Archive on deactivate too? + const [current] = await db.query('SELECT * FROM system_modules WHERE code = ?', [code]); + if (current.length > 0 && current[0].is_active) { + const old = current[0]; + const historySql = ` + INSERT INTO license_history (module_code, license_key, license_type, subscriber_id, activated_at) + VALUES (?, ?, ?, ?, NOW()) + `; + await db.query(historySql, [old.code, old.license_key, old.license_type, old.subscriber_id]); + } + + await db.query('UPDATE system_modules SET is_active = false WHERE code = ?', [code]); + res.json({ success: true, message: 'Module deactivated' }); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Database error' }); + } +}); + + + +module.exports = router; diff --git a/server/schema.sql b/server/schema.sql new file mode 100644 index 0000000..22866b4 --- /dev/null +++ b/server/schema.sql @@ -0,0 +1,53 @@ +-- Database Creation +CREATE DATABASE IF NOT EXISTS smart_asset_db; +USE smart_asset_db; + +-- Assets Table +CREATE TABLE IF NOT EXISTS assets ( + id VARCHAR(20) PRIMARY KEY COMMENT '관리번호 (예: AST-2024-001)', + name VARCHAR(100) NOT NULL COMMENT '자산명', + category VARCHAR(50) NOT NULL COMMENT '카테고리 (설비, IT 등)', + image_url VARCHAR(255) COMMENT '이미지 경로', + model_name VARCHAR(100) COMMENT '모델명', + serial_number VARCHAR(100) COMMENT 'S/N', + manufacturer VARCHAR(100) COMMENT '제작사', + location VARCHAR(100) COMMENT '설치 위치', + purchase_date DATE COMMENT '도입일(입고일)', + purchase_price BIGINT COMMENT '구입 가격', + manager VARCHAR(50) COMMENT '관리책임자', + status ENUM('active', 'maintain', 'broken', 'disposed') DEFAULT 'active' COMMENT '관리상태', + calibration_cycle VARCHAR(50) COMMENT '교정주기', + specs TEXT COMMENT '스펙/사양', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='자산 관리 대장'; + +-- Maintenance History Table (New) +CREATE TABLE IF NOT EXISTS maintenance_history ( + id INT AUTO_INCREMENT PRIMARY KEY, + asset_id VARCHAR(20) NOT NULL COMMENT '자산 ID (FK)', + maintenance_date DATE NOT NULL COMMENT '정비 일자', + type VARCHAR(50) NOT NULL COMMENT '정비 구분 (정기점검, 수리 등)', + content TEXT COMMENT '정비 내용', + image_url VARCHAR(255) COMMENT '첨부 사진', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (asset_id) REFERENCES assets(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='정비 이력'; + +-- Camera Settings Table (New) +CREATE TABLE IF NOT EXISTS camera_settings ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(100) NOT NULL COMMENT '카메라 이름', + ip_address VARCHAR(100) NOT NULL COMMENT 'IP 주소', + port INT DEFAULT 554 COMMENT 'RTSP 포트', + username VARCHAR(100) COMMENT 'RTSP 사용자명', + password VARCHAR(100) COMMENT 'RTSP 비밀번호', + stream_path VARCHAR(200) DEFAULT '/stream1' COMMENT '스트림 경로 (예: /stream1)', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='카메라 설정'; + +-- Sample Data Seeding (Optional) +INSERT INTO assets (id, name, category, model_name, serial_number, manufacturer, location, purchase_date, manager, status, specs) +VALUES +('AST-2024-001', 'CNC 머시닝 센터-0', '설비', 'M-1000', 'S/N-998877', 'HASS', '제1공장 A라인', '2023-11-15', '김담당', 'active', '5축 가공 / 12,000 RPM / CAT40'); diff --git a/server/utils/licenseManager.js b/server/utils/licenseManager.js new file mode 100644 index 0000000..91c4876 --- /dev/null +++ b/server/utils/licenseManager.js @@ -0,0 +1,109 @@ +const crypto = require('crypto'); + +/** + * Generate a signed license key (Base64 Payload + Base64 Signature) + * @param {string} moduleCode - 'asset', 'production', 'monitoring' + * @param {string} type - 'dev', 'sub', 'demo' + * @param {string} subscriberId - Subscriber ID + * @param {number} [activationDays] - Optional + * @param {string} [customExpiryDate] - Optional YYYY-MM-DD + * @param {string|Buffer} privateKey - PEM formatted private key + * @returns {string} Signed License Token + */ +function generateLicense(moduleCode, type, subscriberId, activationDays = null, customExpiryDate = null, privateKey) { + if (!privateKey) { + throw new Error('Private Key is required to generate license.'); + } + + const payloadData = { + m: moduleCode, // Module + t: type, // Type + s: subscriberId, // Subscriber ID + d: Date.now() // Issue Date + }; + + if (customExpiryDate) { + const date = new Date(customExpiryDate + 'T23:59:59'); + if (!isNaN(date.getTime())) payloadData.e = date.getTime(); + } else { + if (type === 'demo') payloadData.e = Date.now() + (30 * 24 * 60 * 60 * 1000); + else if (type === 'sub') payloadData.e = Date.now() + (365 * 24 * 60 * 60 * 1000); + } + + if (activationDays) { + payloadData.ad = Date.now() + (activationDays * 24 * 60 * 60 * 1000); + } + + const payloadStr = JSON.stringify(payloadData); + const sign = crypto.createSign('SHA256'); + sign.update(payloadStr); + sign.end(); + + const signature = sign.sign(privateKey, 'base64'); + const payloadB64 = Buffer.from(payloadStr).toString('base64'); + + return `${payloadB64}.${signature}`; +} + +/** + * Verify a signed license key + * @param {string} licenseToken - The token string (Payload.Signature) + * @param {string|Buffer} publicKey - PEM formatted public key + * @returns {object} { isValid, reason, ...payload } + */ +function verifyLicense(licenseToken, publicKey) { + if (!publicKey) { + return { isValid: false, reason: 'Server Configuration Error: Public Key missing' }; + } + + if (!licenseToken || !licenseToken.includes('.')) { + return { isValid: false, reason: 'Invalid Key Format' }; + } + + const [payloadB64, signature] = licenseToken.split('.'); + + try { + const payloadStr = Buffer.from(payloadB64, 'base64').toString('utf8'); + + // Verify Signature + const verify = crypto.createVerify('SHA256'); + verify.update(payloadStr); + verify.end(); + + const isValidSignature = verify.verify(publicKey, signature, 'base64'); + if (!isValidSignature) { + return { isValid: false, reason: 'Invalid Signature (Key Modified or Wrong Public Key)' }; + } + + // Logic Checks (Expiry, Activation) + const payload = JSON.parse(payloadStr); + const { m, t, d, s, ad, e } = payload; + const subscriberId = s || null; + + if (ad && Date.now() > ad) { + return { isValid: false, reason: `Activation Period Expired (${new Date(ad).toLocaleDateString()})`, module: m, type: t, subscriberId }; + } + + let expiryDate = null; + if (e) { + expiryDate = new Date(e); + if (Date.now() > e) { + return { isValid: false, reason: 'Expired', expiryDate, subscriberId }; + } + } else { + // Fallback for old keys? No, this is a breaking change. Old keys fail signature anyway. + // Just handle 'dev' type (no expiry) + if (t !== 'dev') { + // If not dev and no e, treat as permanent or error? + // Logic above always sets 'e' for sub/demo. + } + } + + return { isValid: true, module: m, type: t, subscriberId, expiryDate }; + + } catch (e) { + return { isValid: false, reason: 'Corrupted Key Payload' }; + } +} + +module.exports = { generateLicense, verifyLicense }; diff --git a/server/utils/remoteLicense.js b/server/utils/remoteLicense.js new file mode 100644 index 0000000..662607f --- /dev/null +++ b/server/utils/remoteLicense.js @@ -0,0 +1,70 @@ +const mysql = require('mysql2/promise'); +require('dotenv').config(); + +// Configuration for the "Vendor/Central" License Database +const remoteConfig = { + host: process.env.LICENSE_DB_HOST, + user: process.env.LICENSE_DB_USER, + password: process.env.LICENSE_DB_PASSWORD, + database: process.env.LICENSE_DB_NAME, + port: process.env.LICENSE_DB_PORT || 3306, + connectTimeout: 5000 // 5 seconds timeout +}; + +function isRemoteConfigured() { + return !!(process.env.LICENSE_DB_HOST && process.env.LICENSE_DB_USER && process.env.LICENSE_DB_NAME); +} + +/** + * Connects to the Remote DB, checks if the key exists, and disconnects. + * @param {string} key + * @returns {Promise} valid + */ +async function checkRemoteKey(key) { + if (!isRemoteConfigured()) { + console.warn('⚠️ Remote License DB not configured. Skipping remote check.'); + return true; // Use local check only if remote is not configured + } + + let conn; + try { + conn = await mysql.createConnection(remoteConfig); + const [rows] = await conn.execute('SELECT 1 FROM issued_licenses WHERE license_key = ? LIMIT 1', [key]); + return rows.length > 0; + } catch (err) { + console.error('❌ Remote License Check Failed:', err.message); + return false; // Fail safe: if we can't verify remotely, assume invalid (or valid? User wanted "management", so fail is safer) + } finally { + if (conn) await conn.end(); + } +} + +/** + * Connects to Remote DB, inserts the generated key, and disconnects. + * @param {object} licenseData { key, module, type } + * @returns {Promise} + */ +async function registerRemoteKey({ key, module, type }) { + if (!isRemoteConfigured()) { + console.log('ℹ️ Remote License DB not configured. Key generated locally only.'); + return; + } + + let conn; + try { + conn = await mysql.createConnection(remoteConfig); + const sql = ` + INSERT INTO issued_licenses (license_key, module_code, license_type, created_at) + VALUES (?, ?, ?, NOW()) + `; + await conn.execute(sql, [key, module, type]); + console.log('✅ License Key registered to Central DB.'); + } catch (err) { + console.error('❌ Failed to register key to Central DB:', err.message); + console.warn('⚠️ The key was generated but may not be activatable if the server enforces remote check.'); + } finally { + if (conn) await conn.end(); + } +} + +module.exports = { checkRemoteKey, registerRemoteKey }; diff --git a/src/app/App.tsx b/src/app/App.tsx new file mode 100644 index 0000000..4a7cef2 --- /dev/null +++ b/src/app/App.tsx @@ -0,0 +1,65 @@ +import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; +import { MainLayout } from '../widgets/layout/MainLayout'; +import { LoginPage } from '../pages/auth/LoginPage'; +import { DashboardPage } from '../modules/asset/pages/DashboardPage'; +import { AssetListPage } from '../modules/asset/pages/AssetListPage'; +import { AssetRegisterPage } from '../modules/asset/pages/AssetRegisterPage'; +import { AssetSettingsPage } from '../modules/asset/pages/AssetSettingsPage'; +import { AssetDetailPage } from '../modules/asset/pages/AssetDetailPage'; +import { UserManagementPage } from '../platform/pages/UserManagementPage'; +import { ProductionPage } from '../production/pages/ProductionPage'; +import { MonitoringPage } from '../modules/cctv/pages/MonitoringPage'; +import { AuthProvider } from '../shared/auth/AuthContext'; +import { AuthGuard } from '../shared/auth/AuthGuard'; + +import { SystemProvider } from '../shared/context/SystemContext'; +import { LicensePage } from '../system/pages/LicensePage'; + +function App() { + return ( + + + + + {/* Public Routes */} + } /> + + {/* Protected Routes */} + + + + }> + } /> + } /> + } /> + {/* Generic List Route */} + } /> + + {/* Specific Category Routes (Reuse List Page) */} + } /> + } /> + } /> + } /> + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + Admin (Coming Soon)} /> + + + {/* Fallback */} + } /> + + + + + ); +} + +export default App; diff --git a/src/app/index.css b/src/app/index.css new file mode 100644 index 0000000..f0023d7 --- /dev/null +++ b/src/app/index.css @@ -0,0 +1,90 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + /* Modern Enterprise Theme (Slate & Sky) */ + /* 눈의 피로를 줄이는 부드러운 회색조 배경과 명확한 가시성의 포인트 컬러 */ + + /* Backgrounds */ + --color-bg-base: #F1F5F9; + /* Slate 100: 눈이 편안한 연한 회색 배경 */ + --color-bg-surface: #ffffff; + /* White: 카드/컨텐츠 영역 */ + --color-bg-sidebar: #1E293B; + /* Slate 800: 안정감 있는 짙은 네이비 (사이드바) */ + + /* Text */ + --color-text-primary: #0F172A; + /* Slate 900: 선명한 검정색 (본문) */ + --color-text-secondary: #64748B; + /* Slate 500: 부드러운 회색 (보조 텍스트) */ + --color-text-inverse: #F8FAFC; + /* Slate 50: 어두운 배경 위 밝은 텍스트 */ + + /* Brand/Accents */ + --color-brand-primary: #0EA5E9; + /* Sky 500: 산뜻하고 눈에 잘 띄는 블루 (강조/버튼) */ + --color-brand-secondary: #0F172A; + /* Slate 900: 보조 강조색 */ + --color-brand-accent: #E2E8F0; + /* Slate 200: 테두리/구분선 */ + + --color-border: #E2E8F0; + + /* Typography */ + --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + + /* Radius */ + --radius-sm: 0.25rem; + --radius-md: 0.375rem; + --radius-lg: 0.5rem; + + /* Transition */ + --transition-base: all 0.2s ease-in-out; +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: var(--font-sans); + background-color: var(--color-bg-base); + color: var(--color-text-primary); + line-height: 1.5; + -webkit-font-smoothing: antialiased; +} + +a { + text-decoration: none; + color: inherit; +} + +button { + cursor: pointer; + border: none; + background: none; + font-family: inherit; +} + +/* Scrollbar */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: #cbd5e1; + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: #94a3b8; +} \ No newline at end of file diff --git a/src/assets/react.svg b/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..d5e482c --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './app/index.css' +import App from './app/App.tsx' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/src/modules/asset/pages/AssetDetailPage.css b/src/modules/asset/pages/AssetDetailPage.css new file mode 100644 index 0000000..7684364 --- /dev/null +++ b/src/modules/asset/pages/AssetDetailPage.css @@ -0,0 +1,210 @@ +/* Page Layout */ +.detail-page .page-header { + margin-bottom: 1rem; + gap: 1rem; + align-items: center; +} + +.back-btn { + padding: 0.5rem; + color: var(--color-text-secondary); +} + +.back-btn:hover { + background-color: #f1f5f9; + color: var(--color-brand-primary); +} + +/* Tabs */ +.tabs-container { + display: flex; + gap: 0.25rem; + margin-bottom: 1rem; + border-bottom: 1px solid var(--color-border); +} + +.tab-btn { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.75rem 1.25rem; + font-size: 0.95rem; + font-weight: 500; + color: var(--color-text-secondary); + background: transparent; + border: none; + border-bottom: 2px solid transparent; + cursor: pointer; + transition: all 0.2s; +} + +.tab-btn:hover { + color: var(--color-brand-primary); + background-color: rgba(59, 130, 246, 0.05); +} + +.tab-btn.active { + color: var(--color-brand-primary); + border-bottom-color: var(--color-brand-primary); + font-weight: 600; +} + +/* Document Layout Structure */ +/* Document Layout Structure */ +.document-layout { + display: flex; + flex-direction: row; + gap: 1.5rem; + align-items: flex-start; +} + +.doc-header-row { + display: flex; + justify-content: flex-end; +} + +.id-table { + width: auto; + min-width: 250px; +} + +.doc-main-row { + display: flex; + gap: 1.5rem; + height: 320px; + /* Fixed height to align image and table */ +} + +.doc-image-area { + flex: 0 0 450px; + /* Width for image area */ + background-color: #f8fafc; + border: 1px solid var(--color-border); + display: flex; + justify-content: center; + align-items: center; + overflow: hidden; +} + +.asset-detail-image { + width: 100%; + height: 100%; + object-fit: contain; + /* Changed from cover to contain */ +} + +.doc-info-area { + flex: 1; +} + +/* Document Tables */ +.doc-table { + width: 100%; + border-collapse: collapse; + font-size: 0.9rem; + border: 1px solid #cbd5e1; +} + +.doc-table th, +.doc-table td { + border: 1px solid #cbd5e1; + padding: 0.75rem 1rem; + vertical-align: middle; +} + +.doc-table th { + font-weight: 600; + color: #475569; + background-color: #f8fafc; + text-align: center; + white-space: nowrap; +} + +.doc-table td { + color: #334155; +} + +.section-divider { + height: 1px; + background-color: var(--color-border); + margin: 1rem 0; +} + +.section-title { + font-size: 1.1rem; + font-weight: 600; + color: var(--color-text-primary); + margin-bottom: 0.5rem; +} + +/* Truncate content in listings */ +.truncate-cell { + max-width: 400px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* Force full width for inputs in table */ +.doc-table input, +.doc-table select { + width: 100% !important; + min-width: 0 !important; + /* Critical for table-fixed */ + box-sizing: border-box !important; + max-width: 100% !important; + height: 100% !important; + /* Ensure it fills the cell height */ + margin: 0 !important; + /* Remove any default margins */ +} + +/* Ensure table layout is strict */ +.doc-table { + table-layout: fixed !important; +} + +/* Maintenance Form */ +.maintenance-form { + animation: slideDown 0.3s ease-out; +} + +@keyframes slideDown { + from { + opacity: 0; + transform: translateY(-10px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Print Overrides */ +@media print { + .page-container { + padding: 0; + background: white; + } + + .page-header, + .tabs-container, + .back-btn { + display: none !important; + } + + .print-friendly { + box-shadow: none !important; + border: none !important; + } + + .doc-main-row { + height: auto; + } + + .doc-image-area { + height: 300px; + border: 1px solid #000; + } +} \ No newline at end of file diff --git a/src/modules/asset/pages/AssetDetailPage.tsx b/src/modules/asset/pages/AssetDetailPage.tsx new file mode 100644 index 0000000..5a1742a --- /dev/null +++ b/src/modules/asset/pages/AssetDetailPage.tsx @@ -0,0 +1,986 @@ +import { useState, useEffect } from 'react'; +import { useParams } from 'react-router-dom'; +import { Card } from '../../../shared/ui/Card'; +import { Button } from '../../../shared/ui/Button'; +import { ArrowLeft, Save, Upload, X, FileText, Wrench, BookOpen, Trash2, ZoomIn, FileSpreadsheet, File, Download, Printer, Plus } from 'lucide-react'; + +import { getMaintenanceTypes } from './AssetSettingsPage'; +import './AssetDetailPage.css'; +import { assetApi } from '../../../shared/api/assetApi'; +import type { Asset, MaintenanceRecord, Manual } from '../../../shared/api/assetApi'; +import { SERVER_URL } from '../../../shared/api/client'; +import { createPortal } from 'react-dom'; +import DatePicker from 'react-datepicker'; +import "react-datepicker/dist/react-datepicker.css"; +import { ko } from 'date-fns/locale'; + +export function AssetDetailPage() { + const { assetId } = useParams<{ assetId: string }>(); + // const navigate = useNavigate(); // Kept for future use + const [asset, setAsset] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + const [activeTab, setActiveTab] = useState<'basic' | 'history' | 'manual'>('basic'); + + useEffect(() => { + if (assetId) { + loadAsset(assetId); + } + }, [assetId]); + + const loadAsset = async (id: string) => { + try { + setIsLoading(true); + const data = await assetApi.getById(id); + setAsset(data); + } catch (error) { + console.error("Failed to load asset:", error); + } finally { + setIsLoading(false); + } + }; + + if (isLoading) return
불러오는 중...
; + if (!asset) return
자산 정보를 찾을 수 없습니다.
; + + // Extend asset with mock data for missing DB fields (consumables, history) + const fullAsset = { + ...asset, + // If image exists, use it. If relative, prepend SERVER_URL. If not, use placeholder. + image: asset.image + ? (asset.image.startsWith('http') ? asset.image : `${SERVER_URL}${asset.image} `) + : 'https://via.placeholder.com/400x350?text=No+Image', + consumables: [], + history: [] + }; + + const renderContent = () => { + switch (activeTab) { + case 'basic': return assetId && loadAsset(assetId)} />; + case 'history': return ; + case 'manual': return ; + default: return assetId && loadAsset(assetId)} />; + } + }; + + return ( +
+
+ + + +
+ +
+ {renderContent()} +
+
+ ); +} + +function BasicInfoTab({ asset, onRefresh }: { asset: Asset & { image?: string, consumables?: any[] }, onRefresh: () => void }) { + const [isEditing, setIsEditing] = useState(false); + const [editData, setEditData] = useState(asset); + const [isZoomed, setIsZoomed] = useState(false); + + const handleChange = ( + e: React.ChangeEvent + ) => { + const { name, value } = e.target; + setEditData(prev => ({ ...prev, [name]: value })); + }; + + const handleSave = async () => { + try { + await assetApi.update(asset.id, editData); + setIsEditing(false); + onRefresh(); // Refresh data from server + } catch (error) { + console.error("Failed to update asset:", error); + alert("저장에 실패했습니다."); + } + }; + + const handleImageUpload = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + try { + const res = await assetApi.uploadImage(file); + // res.data.url is native path like /uploads/filename. We save this to DB. + setEditData(prev => ({ ...prev, image: res.data.url })); + } catch (error) { + console.error("Failed to upload image:", error); + alert("이미지 업로드에 실패했습니다."); + } + }; + + const handleDeleteImage = () => { + if (confirm("이미지를 삭제하시겠습니까?")) { + setEditData(prev => ({ ...prev, image: '' })); + } + }; + + const handleCancel = () => { + setEditData(asset); + setIsEditing(false); + }; + + return ( + <> +
+ {isEditing ? ( + <> + + + + ) : ( + + )} + +
+ + + +
+ {/* Left: Image Area */} +
!isEditing && editData.image && setIsZoomed(true)} + > + {editData.image ? ( + <> + {asset.name} + {!isEditing && ( +
+
+ +
+
+ )} + + ) : ( +
이미지 없음
+ )} + + {isEditing && ( +
e.stopPropagation()}> + + {editData.image && ( + + )} +
+ )} +
+ + {/* Right: Info Table */} +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
관리번호 +
+ {editData.id} +
+
구입가격 + {isEditing ? ( + + ) : ( +
+ {editData.purchasePrice ? `${Number(editData.purchasePrice).toLocaleString()} 원` : '-'} +
+ )} +
설비명 + {isEditing ? ( + + ) : ( +
+ {editData.name} +
+ )} +
S/N + {isEditing ? ( + + ) : ( +
+ {editData.serialNumber} +
+ )} +
입고일 + {isEditing ? ( + + ) : ( +
+ {editData.purchaseDate} +
+ )} +
모델명 + {isEditing ? ( + + ) : ( +
+ {editData.model} +
+ )} +
제작사 + {isEditing ? ( + + ) : ( +
+ {editData.manufacturer} +
+ )} +
교정주기 + {isEditing ? ( + + ) : ( +
+ {editData.calibrationCycle} +
+ )} +
스펙/사양 + {isEditing ? ( + + ) : ( +
+ {editData.specs} +
+ )} +
설치위치 + {isEditing ? ( + + ) : ( +
+ {editData.location} +
+ )} +
관리책임자 + {isEditing ? ( + + ) : ( +
+ {editData.manager} +
+ )} +
관리상태 + {isEditing ? ( +
+ +
+ ) : ( +
+ + {editData.status === 'active' ? '정상 가동' : editData.status === 'disposed' ? '폐기 (말소)' : editData.status === 'maintain' ? '점검 중' : '수리 필요'} + +
+ )} +
+
+
+ +
+ + {/* Consumables Section */} +
+
+

관련 소모품 관리

+ +
+ + + + + + + + + + + {asset.consumables?.map(item => ( + + + + + + + ))} + +
품명규격현재고관리
{item.name}{item.spec}{item.qty}개 + +
+
+
+ + {/* Image Zoom Modal - Moved to Portal */} + {isZoomed && editData.image && createPortal( +
setIsZoomed(false)} + > +
e.stopPropagation()} + > + {/* Modal Header */} +
+

{asset.name} - 이미지 상세

+ +
+ + {/* Modal Content */} +
+ {asset.name} +
+
+
, + document.body + )} + + ); +} + + + +// Helper for badge colors +// Helper for badge colors +const getTypeBadgeColor = (type: string) => { + const types = getMaintenanceTypes(); + const found = types.find(t => t.name === type); + if (!found) return 'bg-slate-100 text-slate-700'; + + switch (found.color) { + case 'success': return 'bg-green-100 text-green-700'; + case 'danger': return 'bg-red-100 text-red-700'; + case 'warning': return 'bg-orange-100 text-orange-700'; + case 'primary': return 'bg-blue-100 text-blue-700'; + default: return 'bg-slate-100 text-slate-700'; // neutral + } +}; + +function HistoryTab({ assetId }: { assetId: string }) { + const [history, setHistory] = useState([]); + const [isWriting, setIsWriting] = useState(true); // Default to open + const [formData, setFormData] = useState({ + maintenance_date: new Date().toISOString().split('T')[0], + type: getMaintenanceTypes()[0]?.name || '정기점검', + content: '', + images: [] as string[] + }); + + useEffect(() => { + loadHistory(); + }, [assetId]); + + const loadHistory = async () => { + try { + const data = await assetApi.getMaintenanceHistory(assetId); + setHistory(data); + } catch (error) { + console.error("Failed to load history:", error); + } + }; + + const handleImageUpload = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + try { + const res = await assetApi.uploadImage(file); + setFormData(prev => ({ ...prev, images: [...(prev.images || []), res.data.url] })); + // Reset input value to allow same file selection again + e.target.value = ''; + } catch (error) { + console.error("Failed to upload image:", error); + alert("이미지 업로드에 실패했습니다."); + } + }; + + const handleDeleteImage = (index: number) => { + setFormData(prev => ({ + ...prev, + images: prev.images.filter((_, i) => i !== index) + })); + }; + + const handleSubmit = async () => { + if (!formData.content) { + alert("정비 내용을 입력해주세요."); + return; + } + try { + await assetApi.addMaintenance(assetId, { + maintenance_date: formData.maintenance_date, + type: formData.type, + content: formData.content, + images: formData.images + }); + setIsWriting(false); + setFormData({ + maintenance_date: new Date().toISOString().split('T')[0], + type: '정기점검', + content: '', + images: [] + }); + loadHistory(); + } catch (error) { + console.error("Failed to save maintenance record:", error); + alert("저장에 실패했습니다."); + } + }; + + const handleDelete = async (id: number) => { + if (confirm("정비 이력을 삭제하시겠습니까?")) { + try { + await assetApi.deleteMaintenance(id); + loadHistory(); + } catch (error) { + console.error("Failed to delete record:", error); + alert("삭제에 실패했습니다."); + } + } + }; + + return ( + +
+

+ + 정비 및 수리 이력 +

+
+ {isWriting && ( + + )} + +
+
+ + {isWriting && ( +
+
+
+
+ + { + if (date) { + const yyyy = date.getFullYear(); + const mm = String(date.getMonth() + 1).padStart(2, '0'); + const dd = String(date.getDate()).padStart(2, '0'); + setFormData({ ...formData, maintenance_date: `${yyyy}-${mm}-${dd}` }); + } + }} + /> +
+
+ + +
+
+ +
+ + +
+ +
+ +
+ + +
+ {formData.images && formData.images.map((imgUrl, idx) => ( +
+
+ {`preview-${idx}`} +
+ +
+ ))} +
+
+
+ + +
+
+ )} + +
+ + + + + + + + + + + + {history.length === 0 ? ( + + + + ) : ( + history.map(item => ( + + + + + + + + )) + )} + +
정비일자구분정비내용첨부관리
+
+
+ +
+

등록된 정비 이력이 없습니다.

+
+
+ {new Date(item.maintenance_date).toLocaleDateString()} + + + {item.type} + + + {item.content} + +
+ {(item.images && item.images.length > 0) ? ( + item.images.map((img, i) => ( + + + + )) + ) : ( + item.image_url ? ( + + + + ) : - + )} +
+
+ +
+
+
+ ); +} + +function ManualTab({ assetId }: { assetId: string }) { + const [manuals, setManuals] = useState([]); + + useEffect(() => { + loadManuals(); + }, [assetId]); + + const loadManuals = async () => { + try { + const data = await assetApi.getManuals(assetId); + setManuals(data); + } catch (error) { + console.error("Failed to load manuals:", error); + } + }; + + const handleFileUpload = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + try { + // Reusing uploadImage since it returns {url} and works for files + const res = await assetApi.uploadImage(file); + await assetApi.addManual(assetId, { + file_name: file.name, + file_url: res.data.url + }); + loadManuals(); + e.target.value = ''; // Reset input + } catch (error) { + console.error("Failed to upload manual:", error); + alert("파일 업로드에 실패했습니다."); + } + }; + + const handleDelete = async (id: number) => { + if (confirm("매뉴얼을 삭제하시겠습니까?")) { + try { + await assetApi.deleteManual(id); + loadManuals(); + } catch (error) { + console.error("Failed to delete manual:", error); + alert("삭제에 실패했습니다."); + } + } + }; + + const getFileIcon = (filename: string) => { + const ext = filename.split('.').pop()?.toLowerCase(); + if (ext === 'pdf') return ; + if (['xls', 'xlsx', 'csv'].includes(ext || '')) return ; + return ; + }; + + return ( + +
+

+ + 매뉴얼 및 지침서 +

+
+ +
+
+ +
+ {manuals.length === 0 ? ( +
+
+ +
+

등록된 매뉴얼이 없습니다.

+
+ ) : ( +
+ {manuals.map(manual => ( +
+
+
+ {getFileIcon(manual.file_name)} +
+
+

window.open(manual.file_url.startsWith('http') ? manual.file_url : `${SERVER_URL}${manual.file_url}`, '_blank')} + > + {manual.file_name} +

+ {new Date(manual.created_at || '').toLocaleDateString()} +
+
+
+ + + + +
+
+ ))} +
+ )} +
+
+ ); +} diff --git a/src/modules/asset/pages/AssetListPage.css b/src/modules/asset/pages/AssetListPage.css new file mode 100644 index 0000000..6f259ab --- /dev/null +++ b/src/modules/asset/pages/AssetListPage.css @@ -0,0 +1,330 @@ +/* Page Layout */ +.page-container { + display: flex; + flex-direction: column; + gap: 1.5rem; + height: 100%; +} + +.page-header { + display: flex; + justify-content: flex-start; + align-items: flex-end; + text-align: left; + /* Explicitly set text align */ +} + +.page-title-text { + font-size: 1.5rem; + font-weight: 700; + color: var(--color-text-primary); + margin-bottom: 0.25rem; +} + +.page-subtitle { + color: var(--color-text-secondary); + font-size: 0.9rem; +} + +.content-card { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; + padding: 0 !important; +} + +/* Toolbar */ +.table-toolbar { + padding: 1.25rem 2rem; + border-bottom: 1px solid var(--color-border); + display: flex; + justify-content: space-between; + align-items: center; + background-color: #fff; + position: relative; + /* For z-index context */ +} + +.search-box { + width: 400px; + max-width: 100%; +} + +.filter-actions { + display: flex; + gap: 0.75rem; + align-items: center; + position: relative; + /* For popup positioning */ +} + +/* Filter Popup */ +.filter-popup { + position: absolute; + top: 110%; + right: 0; + width: 280px; + background-color: white; + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + box-shadow: var(--shadow-lg); + z-index: 50; + padding: 1rem; + animation: fadeIn 0.15s ease-out; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(-5px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +.filter-section { + margin-bottom: 0.75rem; +} + +.filter-title { + font-size: 0.85rem; + font-weight: 600; + color: #64748b; + margin-bottom: 0.5rem; +} + +.filter-options { + display: flex; + flex-direction: column; + gap: 0.35rem; +} + +.checkbox-label { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.9rem; + color: var(--color-text-primary); + cursor: pointer; + user-select: none; +} + +.checkbox-label input { + accent-color: var(--color-brand-primary); + cursor: pointer; +} + +.filter-divider { + height: 1px; + background-color: #f1f5f9; + margin: 0.75rem 0; +} + +.filter-footer { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 1rem; + padding-top: 0.5rem; + border-top: 1px solid #f1f5f9; +} + +.text-btn { + background: none; + border: none; + color: #64748b; + cursor: pointer; + font-size: 0.85rem; + padding: 0; +} + +.text-btn:hover { + color: var(--color-brand-primary); + text-decoration: underline; +} + +/* Table Styles */ +.table-container { + flex: 1; + overflow: auto; +} + +.data-table { + width: 100%; + border-collapse: collapse; + font-size: 0.9rem; +} + +.data-table th { + background-color: #f8fafc; + color: #64748b; + font-weight: 600; + text-align: center; + padding: 0.875rem 1rem; + border-bottom: 1px solid var(--color-border); + position: sticky; + top: 0; + z-index: 10; + white-space: nowrap; +} + +.data-table td { + padding: 0.875rem 1rem; + border-bottom: 1px solid #f1f5f9; + vertical-align: middle; + color: var(--color-text-primary); +} + +.hover-row:hover { + background-color: #f8fafc; +} + +.empty-state { + text-align: center; + padding: 3rem; + color: #94a3b8; +} + +.font-mono { + font-family: monospace; +} + +.text-slate-600 { + color: #475569; +} + +.text-slate-900 { + color: #0f172a; +} + +.text-slate-500 { + color: #64748b; +} + +.text-sm { + font-size: 0.8rem; +} + +.font-medium { + font-weight: 500; +} + +/* Status Badges */ +.data-table td.status-cell { + text-align: center !important; +} + +.data-table td.status-cell>div { + display: flex; + justify-content: center; + width: 100%; +} + +.badge { + display: inline-flex; + padding: 0.25rem 0.6rem; + border-radius: 999px; + font-size: 0.75rem; + font-weight: 600; + white-space: nowrap; +} + +.badge-success { + background-color: #dcfce7; + color: #166534; +} + +.badge-warning { + background-color: #ffedd5; + color: #9a3412; +} + +.badge-danger { + background-color: #fee2e2; + color: #991b1b; +} + +.badge-neutral { + background-color: #f1f5f9; + color: #64748b; +} + +/* Pagination */ +.pagination-bar { + padding: 0.75rem 1.5rem; + border-top: 1px solid var(--color-border); + display: flex; + justify-content: space-between; + align-items: center; + background-color: #fff; +} + +.pagination-info { + font-size: 0.875rem; + color: var(--color-text-secondary); +} + +.pagination-controls { + display: flex; + gap: 0.25rem; +} + +.page-btn { + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--radius-sm); + color: #64748b; + transition: all 0.2s; + border: 1px solid transparent; +} + +.page-btn:hover:not(:disabled) { + background-color: #f1f5f9; + color: #0f172a; +} + +.page-btn:disabled { + color: #cbd5e1; + cursor: not-allowed; +} + +.page-number { + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--radius-sm); + font-size: 0.875rem; + color: #64748b; + transition: all 0.2s; +} + +.page-number:hover { + background-color: #f1f5f9; +} + +.page-number.active { + color: white; + font-weight: 600; +} + +/* Table Utilities */ +.text-center { + text-align: center; +} + +.hover-row:hover td { + background-color: #f8fafc; +} + +.hover-row td button { + white-space: nowrap; +} \ No newline at end of file diff --git a/src/modules/asset/pages/AssetListPage.tsx b/src/modules/asset/pages/AssetListPage.tsx new file mode 100644 index 0000000..64aaecd --- /dev/null +++ b/src/modules/asset/pages/AssetListPage.tsx @@ -0,0 +1,459 @@ +import { useState, useEffect } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { Card } from '../../../shared/ui/Card'; +import { Button } from '../../../shared/ui/Button'; +import { Input } from '../../../shared/ui/Input'; +import { Search, Plus, Filter, Download, ChevronsLeft, ChevronsRight, ChevronLeft, ChevronRight } from 'lucide-react'; +import './AssetListPage.css'; + +import { getCategories } from './AssetSettingsPage'; + +import { assetApi, type Asset } from '../../../shared/api/assetApi'; +import { SERVER_URL } from '../../../shared/api/client'; +import ExcelJS from 'exceljs'; +import { saveAs } from 'file-saver'; + +// Mock Data Removed - Now using API + + +export function AssetListPage() { + const location = useLocation(); + const navigate = useNavigate(); + const [searchTerm, setSearchTerm] = useState(''); + const [currentPage, setCurrentPage] = useState(1); + const [assets, setAssets] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const itemsPerPage = 8; + + // Fetch Assets + useEffect(() => { + loadAssets(); + }, []); + + const loadAssets = async () => { + try { + setIsLoading(true); + const data = await assetApi.getAll(); + setAssets(data); + } catch (error: any) { + console.error("Failed to fetch assets:", error); + } finally { + setIsLoading(false); + } + }; + + // 1. Determine Category based on URL & Dynamic Settings + const getCategoryFromPath = () => { + const categories = getCategories(); + + if (location.pathname.includes('facilities')) return categories.find(c => c.menuLink === 'facilities')?.name || '설비'; + if (location.pathname.includes('tools')) return categories.find(c => c.menuLink === 'tools')?.name || '공구'; + if (location.pathname.includes('instruments')) return categories.find(c => c.menuLink === 'instruments')?.name || '계측기'; + if (location.pathname.includes('vehicles')) return categories.find(c => c.menuLink === 'vehicles')?.name || '차량/운반'; + if (location.pathname.includes('general')) return categories.find(c => c.menuLink === 'general')?.name || '일반 자산'; + if (location.pathname.includes('consumables')) return categories.find(c => c.menuLink === 'consumables')?.name || '소모품'; + + return null; // 'All' + }; + + const currentCategory = getCategoryFromPath(); + + // 2. Determine Page Title + const getPageTitle = () => { + if (!currentCategory) return '전체 자산 조회'; + return `${currentCategory} 현황`; + }; + + const [isFilterOpen, setIsFilterOpen] = useState(false); + const [activeFilters, setActiveFilters] = useState({ + status: [] as string[], + location: [] as string[] + }); + + const toggleFilter = (type: 'status' | 'location', value: string) => { + setActiveFilters(prev => { + const current = prev[type]; + const updated = current.includes(value) + ? current.filter(item => item !== value) + : [...current, value]; + return { ...prev, [type]: updated }; + }); + }; + + const clearFilters = () => { + setActiveFilters({ status: [], location: [] }); + }; + + // 3. Main Filter Logic + const filteredAssets = assets.filter(asset => { + // Category Filter + if (currentCategory) { + // Strict match for category name + // Note: If using multiple categories per item, use includes. For now, strict match. + // If asset.category contains the currentCategory string (e.g. '자산' in '일반 자산'), it might pass loosely. + // But we want strict filtering for specific menus. + + // If the asset category is exactly the current category, keep it. + // Or if the asset category includes the main part (legacy logic). + // Let's make it strict if possible, or robust for '설비' vs '설비 자산'. + + if (asset.category !== currentCategory) { + // Fallback for partial matches if needed, but let's try strict first based on user request. + if (!asset.category.includes(currentCategory)) return false; + } + } + + // Search Term Filter + if (searchTerm) { + const lowerTerm = searchTerm.toLowerCase(); + if (!asset.name.toLowerCase().includes(lowerTerm) && + !asset.id.toLowerCase().includes(lowerTerm) && + !asset.location.toLowerCase().includes(lowerTerm)) { + return false; + } + } + + // Status Filter + if (activeFilters.status.length > 0 && !activeFilters.status.includes(asset.status)) { + return false; + } + + // Location Filter + if (activeFilters.location.length > 0 && !activeFilters.location.includes(asset.location)) { + return false; + } + + return true; + }); + + // 4. Pagination + const totalPages = Math.ceil(filteredAssets.length / itemsPerPage); + const paginatedAssets = filteredAssets.slice( + (currentPage - 1) * itemsPerPage, + currentPage * itemsPerPage + ); + + const getStatusBadge = (status: string) => { + switch (status) { + case 'active': return 정상 가동; + case 'maintain': return 점검 중; + case 'broken': return 수리 필요; + default: return 미상; + } + }; + + const getStatusText = (status: string) => { + switch (status) { + case 'active': return '정상 가동'; + case 'maintain': return '점검 중'; + case 'broken': return '수리 필요'; + case 'disposed': return '폐기 (말소)'; + default: return '미상'; + } + }; + + const handleExcelDownload = async () => { + setIsLoading(true); + try { + const workbook = new ExcelJS.Workbook(); + const worksheet = workbook.addWorksheet('자산 목록'); + + // Columns + worksheet.columns = [ + { header: '이미지', key: 'image', width: 15 }, + { header: '관리번호', key: 'id', width: 20 }, + { header: '자산명', key: 'name', width: 30 }, + { header: '카테고리', key: 'category', width: 15 }, + { header: '모델명', key: 'model', width: 20 }, + { header: 'S/N', key: 'serialNumber', width: 20 }, + { header: '제작사', key: 'manufacturer', width: 20 }, + { header: '설치 위치', key: 'location', width: 20 }, + { header: '상태', key: 'status', width: 15 }, + { header: '관리자', key: 'manager', width: 15 }, + { header: '도입일', key: 'purchaseDate', width: 15 } + ]; + + // Add rows first (Pass 1) to avoid empty row creation issues + const rowsWithImages: { row: ExcelJS.Row, image: string | undefined, id: string }[] = []; + + for (const asset of filteredAssets) { + const row = worksheet.addRow({ + id: asset.id, + name: asset.name, + category: asset.category, + model: asset.model, + serialNumber: asset.serialNumber, + manufacturer: asset.manufacturer, + location: asset.location, + status: getStatusText(asset.status), + manager: asset.manager, + purchaseDate: asset.purchaseDate + }); + + // Set row height + row.height = 60; + + if (asset.image) { + rowsWithImages.push({ row, image: asset.image, id: asset.id }); + } + } + + // Embed Images (Pass 2) + for (const { row, image, id } of rowsWithImages) { + if (!image) continue; + try { + const imageUrl = image.startsWith('http') ? image : `${SERVER_URL}${image}`; + const response = await fetch(imageUrl); + const buffer = await response.arrayBuffer(); + const imageId = workbook.addImage({ + buffer: buffer, + extension: 'png', + }); + + worksheet.addImage(imageId, { + tl: { col: 0, row: row.number - 1 } as any, + br: { col: 1, row: row.number } as any, + editAs: 'oneCell' + }); + } catch (err) { + console.warn('Failed to embed image for asset:', id, err); + } + } + + // Headers Styling + worksheet.getRow(1).font = { bold: true }; + worksheet.getRow(1).alignment = { vertical: 'middle', horizontal: 'center' }; + + // Generate File + const buffer = await workbook.xlsx.writeBuffer(); + const blob = new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }); + const today = new Date().toISOString().split('T')[0].replace(/-/g, ''); + saveAs(blob, `자산목록_${today}.xlsx`); + + } catch (error) { + console.error("Excel generation failed:", error); + alert("엑셀 다운로드 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + }; + + return ( +
+
+
+

{getPageTitle()}

+

+ {currentCategory ? `${currentCategory} 카테고리에 등록된 자산 목록입니다.` : '전체 자산의 실시간 현황을 조회합니다.'} +

+
+
+ + + {/* Toolbar */} +
+
+ } + value={searchTerm} + onChange={(e) => { setSearchTerm(e.target.value); setCurrentPage(1); }} + /> +
+
+ + + + + {/* Filter Popup */} + {isFilterOpen && ( +
+
+

상태

+
+ {[ + { label: '정상 가동', value: 'active' }, + { label: '점검 중', value: 'maintain' }, + { label: '수리 필요', value: 'broken' }, + { label: '폐기 (말소)', value: 'disposed' } + ].map(opt => ( + + ))} +
+
+
+
+

위치

+
+ {[ + '제1공장 A라인', + '제1공장 B라인', + '제2공장 창고', + '본사 사무실' + ].map(loc => ( + + ))} +
+
+
+ + +
+
+ )} +
+
+ + {/* Table */} +
+ + + + + + + + + + + + + + + + + {isLoading ? ( + + + + ) : paginatedAssets.length > 0 ? ( + paginatedAssets.map((asset) => ( + + + + + + + + + + + + + )) + ) : ( + + + + )} + +
관리번호자산명카테고리모델명설치 위치상태관리자도입일관리
+ 데이터를 불러오는 중입니다... +
+ navigate(`/asset/detail/${asset.id}`)} + title="상세 정보 보기" + > + {asset.id} + + + navigate(`/asset/detail/${asset.id}`)} + title="상세 정보 보기" + > + {asset.name} + + {asset.category}{asset.model}{asset.location} +
+ {getStatusBadge(asset.status)} +
+
{asset.manager}{asset.purchaseDate} + +
+ 데이터가 없습니다. +
+
+ + {/* Pagination */} +
+
+ 총 {filteredAssets.length}개 중 {filteredAssets.length > 0 ? (currentPage - 1) * itemsPerPage + 1 : 0} - {Math.min(currentPage * itemsPerPage, filteredAssets.length)} +
+
+ + + + {Array.from({ length: totalPages }, (_, i) => i + 1).map(page => ( + + ))} + + + +
+
+ +
+ ); +} diff --git a/src/modules/asset/pages/AssetRegisterPage.tsx b/src/modules/asset/pages/AssetRegisterPage.tsx new file mode 100644 index 0000000..67d7159 --- /dev/null +++ b/src/modules/asset/pages/AssetRegisterPage.tsx @@ -0,0 +1,331 @@ +import { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Card } from '../../../shared/ui/Card'; +import { Button } from '../../../shared/ui/Button'; +import { Input } from '../../../shared/ui/Input'; +import { Select } from '../../../shared/ui/Select'; +import { ArrowLeft, Save, Upload } from 'lucide-react'; +import { getCategories, getLocations, getIDRule } from './AssetSettingsPage'; +import { assetApi, type Asset } from '../../../shared/api/assetApi'; + +export function AssetRegisterPage() { + const navigate = useNavigate(); + + // Load Settings Data + const categories = getCategories(); + const locations = getLocations(); + const idRule = getIDRule(); + + const [formData, setFormData] = useState({ + id: '', // Asset ID (Auto-generated) + name: '', + categoryId: '', // Use ID for selection + model: '', + serialNo: '', + locationId: '', // Use ID for selection + manager: '', + status: 'active', + purchaseDate: new Date().toISOString().split('T')[0], // Default to today + purchasePrice: '', + image: null as File | null, + imagePreview: '' as string, + manufacturer: '' + }); + + // Auto-generate Asset ID + useEffect(() => { + // If category is required by rule but not selected, can't generate fully + const hasCategoryRule = idRule.some(r => r.type === 'category'); + if (hasCategoryRule && !formData.categoryId) { + setFormData(prev => ({ ...prev, id: '' })); + return; + } + + const category = categories.find(c => c.id === formData.categoryId); + const year = formData.purchaseDate ? new Date(formData.purchaseDate).getFullYear().toString() : new Date().getFullYear().toString(); + + // Build ID string based on Rule + const generatedId = idRule.map(part => { + if (part.type === 'company') return part.value; // e.g. HK + if (part.type === 'custom') return part.value; + if (part.type === 'separator') return part.value; + if (part.type === 'year') return year; + if (part.type === 'category') return category ? category.code : 'UNKNOWN'; + if (part.type === 'sequence') return part.value; // In real app, we fetch next seq. here we just show the format pattern e.g. 001 + return ''; + }).join(''); + + // Ideally we would fetch the *actual* next sequence from API here. + // For now we assume '001' as a placeholder or "Generating..." + const finalId = generatedId.replace('001', '001'); // Just keeping the placeholder visible for user confirmation + + setFormData(prev => ({ ...prev, id: finalId })); + + }, [formData.categoryId, formData.purchaseDate, idRule, categories]); + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData(prev => ({ + ...prev, + [name]: value + })); + }; + + const handleImageChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + const reader = new FileReader(); + reader.onloadend = () => { + setFormData(prev => ({ + ...prev, + image: file, + imagePreview: reader.result as string + })); + }; + reader.readAsDataURL(file); + } + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + // Validation + if (!formData.categoryId || !formData.name || !formData.locationId) { + alert('필수 항목(카테고리, 자산명, 설치위치)을 입력해주세요.'); + return; + } + + try { + // Map IDs to Names/Codes for Backend + const selectedCategory = categories.find(c => c.id === formData.categoryId); + const selectedLocation = locations.find(l => l.id === formData.locationId); + + // Upload Image if exists + let imageUrl = ''; + if (formData.image) { + const uploadRes = await assetApi.uploadImage(formData.image); + imageUrl = uploadRes.data.url; + } + + const payload: Partial = { + id: formData.id, + name: formData.name, + category: selectedCategory ? selectedCategory.name : '미지정', // Backend expects name + model: formData.model, + serialNumber: formData.serialNo, + location: selectedLocation ? selectedLocation.name : '미지정', + manager: formData.manager, + status: formData.status as Asset['status'], + purchaseDate: formData.purchaseDate, + purchasePrice: formData.purchasePrice ? Number(formData.purchasePrice) : 0, + manufacturer: formData.manufacturer, + image: imageUrl + }; + + await assetApi.create(payload); + + alert(`자산이 성공적으로 등록되었습니다.\n자산번호: ${formData.id}`); + navigate('/asset/list'); + } catch (error) { + console.error('Failed to register asset:', error); + alert('자산 등록에 실패했습니다. 서버 상태를 확인해주세요.'); + } + }; + + return ( +
+
+
+

자산 등록

+

새로운 자산을 시스템에 등록합니다.

+
+
+ + +
+
+ + +
+ {/* Basic Info */} +
+

기본 정보

+
+ + + + + + + + + + + + {/* Image Upload Field */} +
+ +
+ {/* Preview Area - Fixed size container */} +
+ {formData.imagePreview ? ( + Preview + ) : ( +
+ +
+ )} +
+ + {/* Upload Controls - Moved below */} +
+ + + + {formData.image ? formData.image.name : '선택된 파일 없음'} + + + + 지원 형식: JPG, PNG, GIF (최대 5MB) + + + +
+
+
+ + + {/* Management Info */} +
+

관리 정보

+
+ + + + + + + +
+
+
+ ); +} diff --git a/src/modules/asset/pages/AssetSettingsPage.css b/src/modules/asset/pages/AssetSettingsPage.css new file mode 100644 index 0000000..f70dfe2 --- /dev/null +++ b/src/modules/asset/pages/AssetSettingsPage.css @@ -0,0 +1,295 @@ +/* Page Header adjustments for Settings */ +.page-header { + display: flex; + flex-direction: column; + gap: 1rem; + padding-bottom: 0 !important; + /* Override default bottom padding */ + border-bottom: 1px solid var(--color-border); +} + +.header-top { + padding-bottom: 0.5rem; +} + +/* Settings Tabs */ +.settings-tabs { + display: flex; + gap: 1.5rem; + margin-bottom: -1px; + /* Overlap border */ +} + +.settings-tab { + padding: 0.75rem 0.5rem; + font-size: 0.95rem; + font-weight: 500; + color: var(--color-text-secondary); + background: transparent; + border: none; + border-bottom: 3px solid transparent; + cursor: pointer; + transition: all 0.2s; +} + +.settings-tab:hover { + color: var(--color-brand-primary); +} + +.settings-tab.active { + color: var(--color-brand-primary); + font-weight: 700; + border-bottom-color: var(--color-brand-primary); +} + +/* Common Layout */ +.input-row { + display: flex; + gap: 1rem; + align-items: flex-end; + /* Align inputs and button bottom */ + margin-bottom: 1.5rem; + width: 100%; +} + +/* Ensure inputs in the row take full width of their container */ +.input-row .ui-field-container { + margin-bottom: 0; +} + +.row-actions { + display: flex; + gap: 0.5rem; + padding-bottom: 2px; + /* Slight adjustment to align with input border */ +} + +/* Table Styles */ +.table-wrapper { + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + overflow: hidden; +} + +.settings-table { + width: 100%; + border-collapse: collapse; + font-size: 0.9rem; +} + +.settings-table th { + background-color: #f8fafc; + color: #64748b; + font-weight: 600; + text-align: left; + padding: 0.75rem 1rem; + border-bottom: 1px solid var(--color-border); +} + +.settings-table td { + padding: 0.75rem 1rem; + border-bottom: 1px solid #f1f5f9; + color: var(--color-text-primary); + vertical-align: middle; +} + +.settings-table tr:last-child td { + border-bottom: none; +} + +.settings-table tr.editing-row { + background-color: #f0f9ff; +} + +.code-badge { + background-color: #e0f2fe; + color: #0369a1; + padding: 0.125rem 0.5rem; + border-radius: 4px; + font-size: 0.8rem; + font-weight: 600; + font-family: monospace; +} + +.cell-actions { + display: flex; + justify-content: center; + gap: 0.5rem; +} + +.icon-btn { + color: #94a3b8; + background: none; + border: none; + cursor: pointer; + padding: 4px; + border-radius: 4px; + transition: all 0.2s; + display: flex; + align-items: center; + justify-content: center; +} + +.icon-btn:hover { + background-color: #f1f5f9; + color: #0f172a; +} + +.icon-btn.delete:hover { + background-color: #fee2e2; + color: #ef4444; +} + +/* Rule Builder Items */ +.preview-box { + background-color: #f1f5f9; + padding: 1rem; + border-radius: var(--radius-md); + display: flex; + align-items: center; + gap: 0.75rem; + margin-bottom: 1.5rem; + border: 1px solid #e2e8f0; +} + +.preview-label { + font-size: 0.875rem; + font-weight: 600; + color: #64748b; +} + +.preview-code { + font-family: monospace; + font-size: 1.125rem; + font-weight: 700; + color: #0f172a; + letter-spacing: 0.05em; +} + +.rule-builder { + min-height: 80px; + margin-bottom: 1.5rem; +} + +.rule-list { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + align-items: center; + min-height: 60px; + padding: 0.5rem; + border: 2px dashed #e2e8f0; + border-radius: var(--radius-md); + background-color: #fcfcfc; +} + +.rule-item { + display: inline-flex; + align-items: center; + background-color: white; + border: 1px solid #cbd5e1; + border-radius: 999px; + padding: 0.25rem 0.25rem 0.25rem 0.75rem; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.rule-chip { + font-size: 0.8rem; + font-weight: 600; + color: #334155; + margin-right: 0.5rem; +} + +.rule-chip.type-company { + color: #0284c7; +} + +.rule-chip.type-category { + color: #c026d3; +} + +.rule-chip.type-year { + color: #ea580c; +} + +.rule-chip.type-sequence { + color: #16a34a; +} + +.rule-chip.type-separator { + color: #64748b; + font-weight: 800; +} + +.rule-actions { + display: flex; + gap: 2px; +} + +.rule-actions button { + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + color: #94a3b8; + transition: all 0.2s; + background: none; + border: none; +} + +.rule-actions button:hover:not(:disabled) { + background-color: #f1f5f9; + color: #0f172a; +} + +.rule-actions button.delete:hover { + background-color: #fee2e2; + color: #ef4444; +} + +.rule-actions button:disabled { + opacity: 0.3; + cursor: not-allowed; +} + +/* Rule Tools */ +.tools-title { + font-size: 0.9rem; + font-weight: 600; + color: #64748b; + margin-bottom: 0.75rem; +} + +.tools-grid { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + align-items: center; +} + +.custom-tool { + display: flex; + align-items: center; + gap: 0.5rem; + margin-left: 0.5rem; + padding-left: 0.5rem; + border-left: 1px solid #e2e8f0; +} + +.custom-text-input { + height: 32px; + /* Match button height (sm size) */ + padding: 0 0.5rem; + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + font-size: 0.875rem; + width: 150px; + outline: none; + transition: border-color 0.2s; +} + +.custom-text-input:focus { + border-color: var(--color-brand-primary); +} \ No newline at end of file diff --git a/src/modules/asset/pages/AssetSettingsPage.tsx b/src/modules/asset/pages/AssetSettingsPage.tsx new file mode 100644 index 0000000..1c945a6 --- /dev/null +++ b/src/modules/asset/pages/AssetSettingsPage.tsx @@ -0,0 +1,933 @@ +import React, { useState } from 'react'; +import { useSearchParams } from 'react-router-dom'; +import { Card } from '../../../shared/ui/Card'; +import { Button } from '../../../shared/ui/Button'; +import { Input } from '../../../shared/ui/Input'; +import { Plus, Trash2, Edit2, X, Check, GripVertical } from 'lucide-react'; +import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors } from '@dnd-kit/core'; +import type { DragEndEvent } from '@dnd-kit/core'; +import { arrayMove, SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy, horizontalListSortingStrategy, useSortable } from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; +import './AssetSettingsPage.css'; + +// --- DnD Components --- +const SortableItemContext = React.createContext(null); + +function SortableRow({ + id, + children, + className, +}: { + id: string; + children: React.ReactNode; + className?: string; +}) { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.5 : 1, + zIndex: isDragging ? 2 : 1, + position: 'relative' as const, + }; + + return ( + + + {children} + + + ); +} + +function DragHandle() { + const { attributes, listeners } = React.useContext(SortableItemContext); + return ( + + ); +} + +function SortableRuleItem({ id, children, className }: { id: string; children: React.ReactNode; className?: string }) { + const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.5 : 1, + display: 'flex', + }; + + return ( +
+ {children} +
+ ); +} + +// --------------------- + +// Types +interface AssetCategory { + id: string; + name: string; + code: string; + menuLink?: string; +} + +interface AssetLocation { + id: string; + name: string; +} + +interface AssetStatus { + id: string; + name: string; + code: string; + color: string; +} + +type IDRuleComponentType = 'company' | 'category' | 'year' | 'sequence' | 'separator' | 'custom'; + +interface IDRuleComponent { + id: string; + type: IDRuleComponentType; + value: string; + label: string; +} + +// Initial Mock Data +let GLOBAL_CATEGORIES: AssetCategory[] = [ + { id: '1', name: '설비', code: 'FAC', menuLink: 'facilities' }, + { id: '2', name: '공구', code: 'TOL', menuLink: 'tools' }, + { id: '3', name: '계측기', code: 'INS' }, + { id: '4', name: '차량/운반', code: 'VEH' }, + { id: '5', name: '일반 자산', code: 'GEN', menuLink: 'general' }, + { id: '6', name: '소모품', code: 'CSM', menuLink: 'consumables' }, +]; + +let GLOBAL_LOCATIONS: AssetLocation[] = [ + { id: '1', name: '제1공장 A라인' }, + { id: '2', name: '제1공장 B라인' }, + { id: '3', name: '제2공장 창고' }, + { id: '4', name: '본사 사무실' }, +]; + +let GLOBAL_STATUSES: AssetStatus[] = [ + { id: '1', name: '정상 가동', code: 'active', color: 'success' }, + { id: '2', name: '점검 중', code: 'maintain', color: 'warning' }, + { id: '3', name: '수리 필요', code: 'broken', color: 'danger' }, + { id: '4', name: '폐기', code: 'disposed', color: 'neutral' }, +]; + +let GLOBAL_ID_RULE: IDRuleComponent[] = [ + { id: 'r1', type: 'company', value: 'HK', label: '회사약어' }, + { id: 'r2', type: 'separator', value: '-', label: '구분자' }, + { id: 'r3', type: 'category', value: '', label: '카테고리' }, + { id: 'r4', type: 'separator', value: '-', label: '구분자' }, + { id: 'r5', type: 'year', value: 'YYYY', label: '년도' }, + { id: 'r6', type: 'separator', value: '-', label: '구분자' }, + { id: 'r7', type: 'sequence', value: '001', label: '일련번호(3자리)' }, +]; + +export interface AssetMaintenanceType { + id: string; + name: string; + color: string; +} + +let GLOBAL_MAINTENANCE_TYPES: AssetMaintenanceType[] = [ + { id: '1', name: '정기점검', color: 'success' }, + { id: '2', name: '수리', color: 'danger' }, + { id: '3', name: '부품교체', color: 'warning' }, + { id: '4', name: '기타', color: 'neutral' }, +]; + +// EXPORTS REQUIRED BY OTHER PAGES +export const getCategories = () => GLOBAL_CATEGORIES; +export const getLocations = () => GLOBAL_LOCATIONS; +export const getStatuses = () => GLOBAL_STATUSES; +export const getIDRule = () => GLOBAL_ID_RULE; +export const getMaintenanceTypes = () => GLOBAL_MAINTENANCE_TYPES; + +export function AssetSettingsPage() { + const [searchParams] = useSearchParams(); + const activeTab = searchParams.get('tab') || 'basic'; + + // State + const [categories, setCategories] = useState(GLOBAL_CATEGORIES); + const [locations, setLocations] = useState(GLOBAL_LOCATIONS); + const [statuses, setStatuses] = useState(GLOBAL_STATUSES); + const [maintenanceTypes, setMaintenanceTypes] = useState(GLOBAL_MAINTENANCE_TYPES); + const [idRule, setIdRule] = useState(GLOBAL_ID_RULE); + + // Form inputs + const [newCategoryName, setNewCategoryName] = useState(''); + const [newCategoryCode, setNewCategoryCode] = useState(''); + const [newCategoryMenuLink, setNewCategoryMenuLink] = useState(''); + const [editingCatId, setEditingCatId] = useState(null); + + const [newLocationName, setNewLocationName] = useState(''); + const [editingLocId, setEditingLocId] = useState(null); + + const [newStatusName, setNewStatusName] = useState(''); + const [newStatusCode, setNewStatusCode] = useState(''); + const [newStatusColor, setNewStatusColor] = useState('neutral'); + const [editingStatusId, setEditingStatusId] = useState(null); + + const [newMaintName, setNewMaintName] = useState(''); + const [newMaintColor, setNewMaintColor] = useState('success'); + const [editingMaintId, setEditingMaintId] = useState(null); + + const [customRuleText, setCustomRuleText] = useState(''); + const [companyCodeInput, setCompanyCodeInput] = useState('HK'); + + // DnD Sensors + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }) + ); + + const handleDragEnd = (event: DragEndEvent) => { + const { active, over } = event; + if (!over || active.id === over.id) return; + + if (activeTab === 'category') { + setCategories((items) => { + const oldIndex = items.findIndex((i) => i.id === active.id); + const newIndex = items.findIndex((i) => i.id === over.id); + const updated = arrayMove(items, oldIndex, newIndex); + GLOBAL_CATEGORIES = updated; + return updated; + }); + } else if (activeTab === 'location') { + setLocations((items) => { + const oldIndex = items.findIndex((i) => i.id === active.id); + const newIndex = items.findIndex((i) => i.id === over.id); + const updated = arrayMove(items, oldIndex, newIndex); + GLOBAL_LOCATIONS = updated; + return updated; + }); + } else if (activeTab === 'status') { + setStatuses((items) => { + const oldIndex = items.findIndex((i) => i.id === active.id); + const newIndex = items.findIndex((i) => i.id === over.id); + const updated = arrayMove(items, oldIndex, newIndex); + GLOBAL_STATUSES = updated; + return updated; + }); + } else if (activeTab === 'maintenance') { + setMaintenanceTypes((items) => { + const oldIndex = items.findIndex((i) => i.id === active.id); + const newIndex = items.findIndex((i) => i.id === over.id); + const updated = arrayMove(items, oldIndex, newIndex); + GLOBAL_MAINTENANCE_TYPES = updated; + return updated; + }); + } else if (activeTab === 'basic') { + setIdRule((items) => { + const oldIndex = items.findIndex((i) => i.id === active.id); + const newIndex = items.findIndex((i) => i.id === over.id); + const updated = arrayMove(items, oldIndex, newIndex); + GLOBAL_ID_RULE = updated; + return updated; + }); + } + }; + + + // --- Category Logic --- + const addOrUpdateCategory = () => { + if (!newCategoryName || !newCategoryCode) return alert('모두 입력해주세요.'); + + if (editingCatId) { + // Update + const updated = categories.map(c => c.id === editingCatId ? { ...c, name: newCategoryName, code: newCategoryCode, menuLink: newCategoryMenuLink } : c); + setCategories(updated); + GLOBAL_CATEGORIES = updated; + setEditingCatId(null); + } else { + // Add + const newEntry = { id: Date.now().toString(), name: newCategoryName, code: newCategoryCode, menuLink: newCategoryMenuLink }; + const updated = [...categories, newEntry]; + setCategories(updated); + GLOBAL_CATEGORIES = updated; + } + setNewCategoryName(''); + setNewCategoryCode(''); + setNewCategoryMenuLink(''); + }; + + const startEditCategory = (id: string, name: string, code: string, menuLink?: string) => { + setNewCategoryName(name); + setNewCategoryCode(code); + setNewCategoryMenuLink(menuLink || ''); + setEditingCatId(id); + }; + + const cancelEditCategory = () => { + setNewCategoryName(''); + setNewCategoryCode(''); + setNewCategoryMenuLink(''); + setEditingCatId(null); + }; + + const deleteCategory = (id: string) => { + if (confirm('삭제하시겠습니까?')) { + const updated = categories.filter(c => c.id !== id); + setCategories(updated); + GLOBAL_CATEGORIES = updated; + if (editingCatId === id) cancelEditCategory(); + } + }; + + + // --- Location Logic --- + const addOrUpdateLocation = () => { + if (!newLocationName) return alert('장소명을 입력해주세요.'); + + if (editingLocId) { + const updated = locations.map(l => l.id === editingLocId ? { ...l, name: newLocationName } : l); + setLocations(updated); + GLOBAL_LOCATIONS = updated; + setEditingLocId(null); + } else { + const newEntry = { id: Date.now().toString(), name: newLocationName }; + const updated = [...locations, newEntry]; + setLocations(updated); + GLOBAL_LOCATIONS = updated; + } + setNewLocationName(''); + }; + + const startEditLocation = (id: string, name: string) => { + setNewLocationName(name); + setEditingLocId(id); + }; + + const cancelEditLocation = () => { + setNewLocationName(''); + setEditingLocId(null); + }; + + const deleteLocation = (id: string) => { + if (confirm('삭제하시겠습니까?')) { + const updated = locations.filter(l => l.id !== id); + setLocations(updated); + GLOBAL_LOCATIONS = updated; + if (editingLocId === id) cancelEditLocation(); + } + }; + + + // --- Status Functions --- + const startEditStatus = (id: string, name: string, code: string, color: string) => { + setEditingStatusId(id); + setNewStatusName(name); + setNewStatusCode(code); + setNewStatusColor(color); + }; + + const cancelEditStatus = () => { + setEditingStatusId(null); + setNewStatusName(''); + setNewStatusCode(''); + setNewStatusColor('neutral'); + }; + + const addOrUpdateStatus = () => { + if (!newStatusName.trim() || !newStatusCode.trim()) return alert('상태명과 코드를 입력해주세요.'); + + if (editingStatusId) { + // Update + const updated = statuses.map(item => + item.id === editingStatusId + ? { ...item, name: newStatusName, code: newStatusCode, color: newStatusColor } + : item + ); + setStatuses(updated); + GLOBAL_STATUSES = updated; + cancelEditStatus(); + } else { + // Add + const newId = String(Date.now()); + const newItem: AssetStatus = { id: newId, name: newStatusName, code: newStatusCode, color: newStatusColor }; + const updated = [...statuses, newItem]; + setStatuses(updated); + GLOBAL_STATUSES = updated; + setNewStatusName(''); + setNewStatusCode(''); + setNewStatusColor('neutral'); + } + }; + + const deleteStatus = (id: string) => { + if (window.confirm('정말 삭제하시겠습니까?')) { + const updated = statuses.filter(item => item.id !== id); + setStatuses(updated); + GLOBAL_STATUSES = updated; + if (editingStatusId === id) cancelEditStatus(); + } + }; + + + // --- Maintenance Type Logic --- + const startEditMaint = (id: string, name: string, color: string) => { + setEditingMaintId(id); + setNewMaintName(name); + setNewMaintColor(color); + }; + + const cancelEditMaint = () => { + setEditingMaintId(null); + setNewMaintName(''); + setNewMaintColor('neutral'); + }; + + const addOrUpdateMaint = () => { + if (!newMaintName.trim()) return alert('구분명을 입력해주세요.'); + + if (editingMaintId) { + const updated = maintenanceTypes.map(item => + item.id === editingMaintId + ? { ...item, name: newMaintName, color: newMaintColor } + : item + ); + setMaintenanceTypes(updated); + GLOBAL_MAINTENANCE_TYPES = updated; + cancelEditMaint(); + } else { + const newId = String(Date.now()); + const newItem: AssetMaintenanceType = { id: newId, name: newMaintName, color: newMaintColor }; + const updated = [...maintenanceTypes, newItem]; + setMaintenanceTypes(updated); + GLOBAL_MAINTENANCE_TYPES = updated; + setNewMaintName(''); + setNewMaintColor('neutral'); + } + }; + + const deleteMaint = (id: string) => { + if (window.confirm('정말 삭제하시겠습니까?')) { + const updated = maintenanceTypes.filter(item => item.id !== id); + setMaintenanceTypes(updated); + GLOBAL_MAINTENANCE_TYPES = updated; + if (editingMaintId === id) cancelEditMaint(); + } + }; + + + // --- ID Rule Logic --- + const addRuleComponent = (type: IDRuleComponentType, defaultValue: string, label: string) => { + const newComp: IDRuleComponent = { + id: Date.now().toString(), + type, + value: type === 'custom' ? customRuleText : defaultValue, + label + }; + + if (type === 'custom' && !customRuleText) return alert('사용자 정의 텍스트를 입력하세요.'); + + const updated = [...idRule, newComp]; + setIdRule(updated); + GLOBAL_ID_RULE = updated; + setCustomRuleText(''); + }; + + const removeRuleComponent = (id: string) => { + const updated = idRule.filter(comp => comp.id !== id); + setIdRule(updated); + GLOBAL_ID_RULE = updated; + }; + + + const getPreviewId = () => { + const mockData = { + company: 'HK', + category: 'FAC', + year: new Date().getFullYear().toString(), + sequence: '001', + separator: '-' + }; + + return idRule.map(part => { + if (part.type === 'custom') return part.value; + if (part.type === 'company') return part.value; + if (part.type === 'category') return mockData.category; + if (part.type === 'year') return mockData.year; + if (part.type === 'sequence') return part.value; + if (part.type === 'separator') return part.value; + return ''; + }).join(''); + }; + + return ( +
+
+ + {activeTab === 'basic' && ( + +
+

자산 관리번호 생성 규칙

+

자산 등록 시 자동 생성될 관리번호의 포맷을 조합합니다.

+
+
+ {/* Preview Box */} +
+ 생성 예시: + {getPreviewId()} +
+ + {/* Configurator */} +
+
+ {idRule.length === 0 &&
규칙 요소가 없습니다. 아래에서 추가하세요.
} + + {idRule.map((comp) => ( + + + {comp.label} + {(comp.type === 'company' || comp.type === 'custom' || comp.type === 'separator') && ` (${comp.value})`} + +
+ {/* Up/Down buttons removed in favor of DnD */} + +
+
+ ))} +
+
+
+ + {/* Toolset */} +
+

규칙 요소 추가

+
+
+ setCompanyCodeInput(e.target.value.toUpperCase())} + className="custom-text-input" + maxLength={5} + style={{ width: '100px' }} + /> + +
+ + + + +
+ setCustomRuleText(e.target.value)} + className="custom-text-input" + /> + +
+
+
+
+
+ )} + + {activeTab === 'category' && ( + +
+

카테고리 관리

+

자산 유형을 분류하는 카테고리를 관리합니다. (약어는 자산번호 생성 시 사용됩니다)

+
+ +
+
+
+ +
+ setNewCategoryName(e.target.value)} + /> +
+
+
+ +
+ setNewCategoryCode(e.target.value.toUpperCase())} + maxLength={3} + /> +
+
+
+ +
+ +
+
+
+
+ + {editingCatId && ( + + )} +
+
+
+ +
+ + + + + + + + + + + + + {categories.map((cat) => ( + + + + + + + + ))} + + +
카테고리 명약어메뉴 연결관리
{cat.name}{cat.code} + {cat.menuLink ? ( + + {cat.menuLink} + + ) : ( + - + )} + +
+ + +
+
+
+
+
+ )} + + {activeTab === 'location' && ( + +
+

설치 위치 / 보관 장소

+

자산이 위치할 수 있는 장소를 관리합니다.

+
+
+
+
+ setNewLocationName(e.target.value)} + /> +
+
+ + {editingLocId && ( + + )} +
+
+ +
+ + + + + + + + + + + {locations.map((loc) => ( + + + + + + ))} + + +
장소 명관리
{loc.name} +
+ + +
+
+
+
+
+ )} + + {activeTab === 'status' && ( + +
+

자산 상태 관리

+

자산의 상태(운용, 파손, 수리 등)를 정의하고 관리합니다.

+
+
+
+
+ setNewStatusName(e.target.value)} + /> +
+
+ setNewStatusCode(e.target.value)} + /> +
+
+ + +
+
+ + {editingStatusId && ( + + )} +
+
+ +
+ + + + + + + + + + + + + {statuses.map((item) => ( + + + + + + + + ))} + + +
상태코드라벨 미리보기관리
{item.name}{item.code} + + {item.name} + + +
+ + +
+
+
+
+
+ )} + + {activeTab === 'maintenance' && ( + +
+

유지보수 구분 관리

+

유지보수 이력 등록 시 사용할 작업 구분을 관리합니다.

+
+
+
+
+ setNewMaintName(e.target.value)} + /> +
+
+ + +
+
+ + {editingMaintId && ( + + )} +
+
+ +
+ + + + + + + + + + + + {maintenanceTypes.map((item) => ( + + + + + + + ))} + + +
구분명라벨 미리보기관리
{item.name} + + {item.name} + + +
+ + +
+
+
+
+
+ )} +
+
+
+ ); +} diff --git a/src/modules/asset/pages/DashboardPage.css b/src/modules/asset/pages/DashboardPage.css new file mode 100644 index 0000000..82e040b --- /dev/null +++ b/src/modules/asset/pages/DashboardPage.css @@ -0,0 +1,284 @@ +.dashboard-container { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.stats-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 1.5rem; +} + +.stat-card { + background: white; + border-radius: var(--radius-lg); + padding: 1.5rem; + border: 1px solid var(--color-border); + box-shadow: var(--shadow-sm); + display: flex; + gap: 1rem; + align-items: flex-start; + transition: transform 0.2s; +} + +.stat-card:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-md); +} + +.stat-icon { + width: 48px; + height: 48px; + border-radius: var(--radius-md); + display: flex; + align-items: center; + justify-content: center; +} + +.stat-info { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.stat-label { + color: var(--color-text-secondary); + font-size: 0.875rem; + font-weight: 500; +} + +.stat-value { + color: var(--color-text-primary); + font-size: 1.5rem; + font-weight: 700; + line-height: 1.2; +} + +.stat-trend { + font-size: 0.75rem; + display: flex; + align-items: center; + gap: 0.25rem; + margin-top: 0.25rem; + color: var(--color-text-secondary); +} + +.stat-trend.positive { + color: #10b981; + /* Green */ +} + +.stat-trend.negative { + color: #ef4444; + /* Red */ +} + +/* Utility Colors for Icons */ +.bg-blue-50 { + background-color: #eff6ff; +} + +.text-blue-600 { + color: #2563eb; +} + +.bg-red-50 { + background-color: #fef2f2; +} + +.text-red-600 { + color: #dc2626; +} + +.bg-green-50 { + background-color: #f0fdf4; +} + +.text-green-600 { + color: #16a34a; +} + +.bg-purple-50 { + background-color: #faf5ff; +} + +.text-purple-600 { + color: #9333ea; +} + +/* Dashboard Main Grid */ +.dashboard-main-grid { + display: grid; + grid-template-columns: 2fr 1fr; + gap: 1.5rem; +} + +/* Activity List */ +.activity-list { + list-style: none; + display: flex; + flex-direction: column; + gap: 1rem; +} + +.activity-item { + display: flex; + align-items: center; + gap: 1rem; + padding-bottom: 1rem; + border-bottom: 1px solid #f1f5f9; +} + +.activity-item:last-child { + padding-bottom: 0; + border-bottom: none; +} + +.activity-icon { + width: 36px; + height: 36px; + border-radius: 50%; + background-color: #f1f5f9; + color: #64748b; + display: flex; + align-items: center; + justify-content: center; +} + +.activity-details { + flex: 1; +} + +.activity-text { + font-size: 0.9rem; + color: var(--color-text-primary); +} + +.activity-time { + font-size: 0.75rem; + color: var(--color-text-secondary); +} + +.status-badge { + font-size: 0.75rem; + padding: 0.25rem 0.6rem; + border-radius: 999px; + font-weight: 500; + background-color: #f1f5f9; + color: #64748b; +} + +.status-badge.success { + background-color: #dcfce7; + color: #166534; +} + +.status-badge.warning { + background-color: #ffedd5; + color: #9a3412; +} + +.status-badge.danger { + background-color: #fee2e2; + color: #991b1b; +} + +/* Tailwind Utilities (Mock) */ +.bg-white { + background-color: white; +} + +.rounded-lg { + border-radius: var(--radius-lg); +} + +.border { + border: 1px solid; +} + +.border-slate-200 { + border-color: #e2e8f0; +} + +.border-slate-100 { + border-color: #f1f5f9; +} + +.shadow-sm { + box-shadow: var(--shadow-sm); +} + +.p-6 { + padding: 1.5rem; +} + +.px-6 { + padding-left: 1.5rem; + padding-right: 1.5rem; +} + +.py-4 { + padding-top: 1rem; + padding-bottom: 1rem; +} + +.border-b { + border-bottom-width: 1px; +} + +.flex { + display: flex; +} + +.justify-between { + justify-content: space-between; +} + +.items-center { + align-items: center; +} + +.font-semibold { + font-weight: 600; +} + +.text-slate-800 { + color: #1e293b; +} + +.text-slate-500 { + color: #64748b; +} + +.text-red-600 { + color: #dc2626; +} + +.text-orange-500 { + color: #f97316; +} + +.font-bold { + font-weight: 700; +} + +.w-full { + width: 100%; +} + +.text-sm { + font-size: 0.875rem; +} + +.text-left { + text-align: left; +} + +.p-3 { + padding: 0.75rem; +} + +.bg-slate-50 { + background-color: #f8fafc; +} \ No newline at end of file diff --git a/src/modules/asset/pages/DashboardPage.tsx b/src/modules/asset/pages/DashboardPage.tsx new file mode 100644 index 0000000..a7d0227 --- /dev/null +++ b/src/modules/asset/pages/DashboardPage.tsx @@ -0,0 +1,121 @@ +import { Card } from '../../../shared/ui/Card'; +import { Package, AlertTriangle, CheckCircle, TrendingUp, DollarSign } from 'lucide-react'; +import './DashboardPage.css'; + +export function DashboardPage() { + return ( +
+ {/* Stats Row */} +
+
+
+ +
+
+ 총 자산 + 1,248 + + +12% + +
+
+ +
+
+ +
+
+ 정비 필요 + 5 + + 긴급 조치 요망 + +
+
+ +
+
+ +
+
+ 가동률 + 98.5% + + +0.5% + +
+
+ +
+
+ +
+
+ 이번 달 구매 + ₩ 12.5M + + 3건 예정 + +
+
+
+ + {/* Main Grid */} +
+ +
    + {[1, 2, 3, 4, 5].map((i) => ( +
  • +
    + +
    +
    +

    + CNC 머신 #0{i} 정기 점검 완료 +

    +

    2시간 전

    +
    + 완료 +
  • + ))} +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
품목명현재고상태
절삭유 A-Type2 Drum주문필요
베어링 62045 EA부족
장갑 (L)12 Pack정상
+
+
+
+ ); +} + +function WrenchIcon() { + return ( + + ) +} diff --git a/src/modules/cctv/components/JSMpegPlayer.tsx b/src/modules/cctv/components/JSMpegPlayer.tsx new file mode 100644 index 0000000..12e2695 --- /dev/null +++ b/src/modules/cctv/components/JSMpegPlayer.tsx @@ -0,0 +1,71 @@ +import { useEffect, useRef, useState } from 'react'; + + +interface JSMpegPlayerProps { + url: string; + width?: number | string; + height?: number | string; + className?: string; +} + +export function JSMpegPlayer({ url, width, height, className }: JSMpegPlayerProps) { + const canvasRef = useRef(null); + const playerRef = useRef(null); + const [retryCount, setRetryCount] = useState(0); + + useEffect(() => { + if (!canvasRef.current || !url) return; + + let player: any = null; + let reconnectTimer: any = null; + + const initPlayer = () => { + try { + // @ts-ignore + const JSMpeg = window.JSMpeg; + if (!JSMpeg) { + console.error('JSMpeg library not loaded correctly'); + return; + } + + // JSMpeg v2 uses JSMpeg.Player and accepts string URL + player = new JSMpeg.Player(url, { + canvas: canvasRef.current, + autoplay: true, + audio: false, + loop: false, + onSourceCompleted: () => { + console.log('Source completed/Disconnected, reconnecting in 1s...'); + // Trigger usage of reconnect + if (reconnectTimer) clearTimeout(reconnectTimer); + reconnectTimer = setTimeout(() => { + setRetryCount(prev => prev + 1); // Trigger effect re-run + }, 1000); + } + }); + playerRef.current = player; + } catch (e) { + console.error('Failed to init JSMpeg player', e); + } + }; + + initPlayer(); + + return () => { + if (reconnectTimer) clearTimeout(reconnectTimer); + if (player) { + try { + player.destroy(); + } catch (e) { + // Ignore + } + } + }; + }, [url, retryCount]); // Re-run when url or retryCount changes + + return ( +
+ +
+ ); +} diff --git a/src/modules/cctv/pages/MonitoringPage.tsx b/src/modules/cctv/pages/MonitoringPage.tsx new file mode 100644 index 0000000..727fbeb --- /dev/null +++ b/src/modules/cctv/pages/MonitoringPage.tsx @@ -0,0 +1,462 @@ +import { useState, useEffect } from 'react'; +import { apiClient } from '../../../shared/api/client'; +import { JSMpegPlayer } from '../components/JSMpegPlayer'; +import { Plus, Settings, Trash2, X, Video } from 'lucide-react'; +import { useAuth } from '../../../shared/auth/AuthContext'; +import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors, type DragEndEvent } from '@dnd-kit/core'; +import { arrayMove, SortableContext, sortableKeyboardCoordinates, useSortable, rectSortingStrategy } from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; + +interface Camera { + id: number; + name: string; + ip_address: string; + port: number; + username?: string; + password?: string; + stream_path?: string; + transport_mode?: 'tcp' | 'udp' | 'auto'; + rtsp_encoding?: boolean; + quality?: 'low' | 'medium' | 'original'; + display_order?: number; +} + +// Wrap Camera Card with Sortable +function SortableCamera({ camera, children, disabled }: { camera: Camera, children: React.ReactNode, disabled: boolean }) { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + } = useSortable({ id: camera.id, disabled }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + }; + + return ( +
+ {children} +
+ ); +} + +export function MonitoringPage() { + const { user } = useAuth(); + const [cameras, setCameras] = useState([]); + const [showForm, setShowForm] = useState(false); + const [editingCamera, setEditingCamera] = useState(null); + const [formData, setFormData] = useState>({ + name: '', + ip_address: '', + port: 554, + username: '', + password: '', + stream_path: '/stream1', + transport_mode: 'tcp', + rtsp_encoding: false, + quality: 'low' + }); + const [loading, setLoading] = useState(false); + + const [streamVersions, setStreamVersions] = useState<{ [key: number]: number }>({}); + + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }) + ); + + useEffect(() => { + fetchCameras(); + }, []); + + const fetchCameras = async () => { + try { + const res = await apiClient.get('/cameras'); + setCameras(res.data); + } catch (err) { + console.error('Failed to fetch cameras', err); + } + }; + + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData(prev => ({ ...prev, [name]: value })); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + try { + if (editingCamera) { + await apiClient.put(`/cameras/${editingCamera.id}`, formData); + // Force stream refresh for this camera + setStreamVersions(prev => ({ ...prev, [editingCamera.id]: Date.now() })); + } else { + await apiClient.post('/cameras', formData); + } + setShowForm(false); + setEditingCamera(null); + setFormData({ + name: '', + ip_address: '', + port: 554, + username: '', + password: '', + stream_path: '/stream1', + transport_mode: 'tcp', + rtsp_encoding: false, + quality: 'low' + }); + fetchCameras(); + } catch (err) { + console.error('Failed to save camera', err); + const errMsg = (err as any).response?.data?.error || (err as any).message || '카메라 저장 실패'; + alert(`오류 발생: ${errMsg}`); + } finally { + setLoading(false); + } + }; + + const handleEdit = (camera: Camera) => { + setEditingCamera(camera); + setFormData(camera); + setShowForm(true); + }; + + const handleDelete = async (id: number) => { + if (!window.confirm('정말 삭제하시겠습니까?')) return; + try { + await apiClient.delete(`/cameras/${id}`); + fetchCameras(); + } catch (err) { + console.error('Failed to delete camera', err); + } + }; + + const handleDragEnd = async (event: DragEndEvent) => { + const { active, over } = event; + + if (over && active.id !== over.id) { + setCameras((items) => { + const oldIndex = items.findIndex((item) => item.id === active.id); + const newIndex = items.findIndex((item) => item.id === over.id); + const newItems = arrayMove(items, oldIndex, newIndex); + + // Save new order + apiClient.put('/cameras/reorder', { cameras: newItems.map(c => c.id) }) + .catch(e => console.error('Failed to save order', e)); + + return newItems; + }); + } + }; + + const getStreamUrl = (cameraId: number) => { + // Use current host (proxied or direct) + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + const version = streamVersions[cameraId] || 0; + const url = `${protocol}//${window.location.host}/api/stream?cameraId=${cameraId}&v=${version}`; + console.log(`[CCTV] Connecting to: ${url}`); + return url; + }; + + const handlePing = async (id: number) => { + try { + const res = await apiClient.get(`/cameras/${id}/ping`); + + // Critical check: If the server returns HTML (catch-all), it's old code + if (typeof res.data === 'string' && res.data.includes(' { + try { + const res = await apiClient.get('/health'); + console.log('[System] Server Version:', res.data); + } catch (e) { + console.warn('[System] Could not fetch server health - might be old version.'); + } + }; + + useEffect(() => { + fetchCameras(); + checkServerVersion(); + }, []); + + return ( +
+
+

+

+ {user?.role === 'admin' && ( + + )} +
+ + {/* Camera Grid with DnD */} + + c.id)} + strategy={rectSortingStrategy} + > +
+ {cameras.map(camera => ( + +
+ + {/* Overlay Controls */} + {user?.role === 'admin' && ( +
e.stopPropagation()}> + + + +
+ )} +
+

+ + {camera.name} +

+ {/* IP/Port Hidden as requested */} +
+
+
+ ))} +
+
+
+ + {cameras.length === 0 && ( +
+
+ )} + + {/* Add/Edit Modal */} + {showForm && ( +
+
+
+

+ {editingCamera ? '카메라 설정 수정' : '새 카메라 추가'} +

+ +
+ +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+ + +

+ TAPO C200 예시: /stream1 (고화질) 또는 /stream2 (저화질) +

+
+ + {/* Advanced Settings */} +
+

+ 고급 설정 +

+
+
+ + +
+
+ +
+
+ + +
+
+

+ * 연결이 불안정하면 TCP로 설정하세요. 비밀번호에 특수문자가 있어 연결 실패 시 인코딩을 켜보세요. +

+
+ +
+ + +
+
+
+
+ )} +
+ ); +} diff --git a/src/pages/auth/LoginPage.css b/src/pages/auth/LoginPage.css new file mode 100644 index 0000000..1a651d9 --- /dev/null +++ b/src/pages/auth/LoginPage.css @@ -0,0 +1,127 @@ +.login-container { + display: flex; + align-items: center; + justify-content: center; + height: 100vh; + width: 100vw; + background-color: var(--color-bg-base); + background: linear-gradient(135deg, var(--color-bg-sidebar) 0%, var(--color-brand-primary) 100%); +} + +.login-card { + width: 100%; + max-width: 400px; + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(10px); + padding: 3rem; + border-radius: var(--radius-lg); + box-shadow: var(--shadow-lg); +} + +.login-header { + text-align: center; + margin-bottom: 2.5rem; +} + +.brand-logo { + display: inline-flex; + padding: 1rem; + background-color: var(--color-bg-sidebar); + color: var(--color-text-inverse); + border-radius: var(--radius-md); + margin-bottom: 1.5rem; + box-shadow: var(--shadow-md); +} + +.login-header h1 { + font-size: 1.75rem; + font-weight: 700; + color: var(--color-text-primary); + margin-bottom: 0.5rem; +} + +.login-header p { + color: var(--color-text-secondary); + font-size: 0.875rem; +} + +.login-form { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.form-group { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.form-group label { + font-size: 0.875rem; + font-weight: 500; + color: var(--color-text-primary); +} + +.input-wrapper { + position: relative; + display: flex; + align-items: center; +} + +.input-icon { + position: absolute; + left: 1rem; + color: var(--color-text-secondary); + pointer-events: none; +} + +.form-group input { + width: 100%; + padding: 0.75rem 1rem 0.75rem 2.75rem; + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + font-size: 0.9rem; + transition: all 0.2s; + background-color: var(--color-bg-surface); +} + +.form-group input:focus { + outline: none; + border-color: var(--color-brand-primary); + box-shadow: 0 0 0 3px rgba(82, 109, 130, 0.1); +} + +.login-btn { + margin-top: 1rem; + padding: 0.875rem; + background-color: var(--color-bg-sidebar); + color: white; + font-weight: 600; + border-radius: var(--radius-md); + font-size: 1rem; + letter-spacing: 0.025em; + transition: all 0.2s; +} + +.login-btn:hover { + background-color: #1e293b; + transform: translateY(-1px); + box-shadow: var(--shadow-md); +} + +.error-message { + color: #ef4444; + font-size: 0.875rem; + text-align: center; + background-color: rgba(239, 68, 68, 0.1); + padding: 0.75rem; + border-radius: var(--radius-sm); +} + +.login-footer { + margin-top: 2rem; + text-align: center; + font-size: 0.75rem; + color: var(--color-text-secondary); +} \ No newline at end of file diff --git a/src/pages/auth/LoginPage.tsx b/src/pages/auth/LoginPage.tsx new file mode 100644 index 0000000..da46577 --- /dev/null +++ b/src/pages/auth/LoginPage.tsx @@ -0,0 +1,88 @@ +import { useState } from 'react'; +import { useNavigate, useLocation } from 'react-router-dom'; +import { useAuth } from '../../shared/auth/AuthContext'; +import { Box, Lock, User } from 'lucide-react'; +import './LoginPage.css'; + +export function LoginPage() { + const [id, setId] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(''); + const { login } = useAuth(); + const navigate = useNavigate(); + const location = useLocation(); + + const from = location.state?.from?.pathname || '/asset/dashboard'; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(''); + + try { + const success = await login(id, password); + if (success) { + navigate(from, { replace: true }); + } else { + setError('아이디 또는 비밀번호를 확인해주세요.'); + } + } catch (err) { + setError('로그인 중 오류가 발생했습니다.'); + } + }; + + return ( +
+
+
+
+ +
+

SmartAsset

+

통합 자산관리 시스템

+
+ +
+
+ +
+ + setId(e.target.value)} + placeholder="아이디를 입력하세요" + required + /> +
+
+ +
+ +
+ + setPassword(e.target.value)} + placeholder="비밀번호를 입력하세요" + required + /> +
+
+ + {error &&
{error}
} + + +
+ +
+

© 2026 SmartAsset System. All rights reserved.

+
+
+
+ ); +} diff --git a/src/platform/pages/UserManagementPage.css b/src/platform/pages/UserManagementPage.css new file mode 100644 index 0000000..9859320 --- /dev/null +++ b/src/platform/pages/UserManagementPage.css @@ -0,0 +1,27 @@ +/* UserManagementPage.css */ +.page-container { + padding: 24px; + background-color: #f8fafc; + min-height: 100vh; +} + +.text-red-500 { + color: #ef4444; +} + +/* Modal slide-in animation */ +@keyframes slideIn { + from { + opacity: 0; + transform: translateY(20px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +.animate-in { + animation: slideIn 0.3s ease-out forwards; +} \ No newline at end of file diff --git a/src/platform/pages/UserManagementPage.tsx b/src/platform/pages/UserManagementPage.tsx new file mode 100644 index 0000000..59b334a --- /dev/null +++ b/src/platform/pages/UserManagementPage.tsx @@ -0,0 +1,293 @@ +import { useState, useEffect } from 'react'; +import { Card } from '../../shared/ui/Card'; +import { Button } from '../../shared/ui/Button'; +import { Input } from '../../shared/ui/Input'; +import { apiClient } from '../../shared/api/client'; +import { Plus, Edit2, Trash2, X, Check, Shield, User as UserIcon } from 'lucide-react'; +import type { User } from '../../shared/auth/AuthContext'; +import './UserManagementPage.css'; // Will create CSS separately or inline styles initially. Let's assume global css or create specific. + +interface UserFormData { + id: string; + password?: string; + name: string; + department: string; + position: string; + phone: string; + role: 'admin' | 'user'; +} + +export function UserManagementPage() { + const [users, setUsers] = useState([]); + const [loading, setLoading] = useState(false); + + // Modal State + const [isModalOpen, setIsModalOpen] = useState(false); + const [isEditing, setIsEditing] = useState(false); + const [formData, setFormData] = useState({ + id: '', + password: '', + name: '', + department: '', + position: '', + phone: '', + role: 'user' + }); + + useEffect(() => { + fetchUsers(); + }, []); + + const fetchUsers = async () => { + setLoading(true); + try { + const res = await apiClient.get('/users'); + setUsers(res.data); + } catch (error) { + console.error('Failed to fetch users', error); + alert('사용자 목록을 불러오지 못했습니다.'); + } finally { + setLoading(false); + } + }; + + const handleOpenAdd = () => { + setFormData({ + id: '', + password: '', + name: '', + department: '', + position: '', + phone: '', + role: 'user' + }); + setIsEditing(false); + setIsModalOpen(true); + }; + + const handleOpenEdit = (user: User) => { + setFormData({ + id: user.id, + password: '', // Password empty by default on edit + name: user.name, + department: user.department || '', + position: user.position || '', + phone: user.phone || '', + role: user.role + }); + setIsEditing(true); + setIsModalOpen(true); + }; + + const handleDelete = async (id: string) => { + if (!confirm('정말 이 사용자를 삭제하시겠습니까?')) return; + try { + await apiClient.delete(`/users/${id}`); + fetchUsers(); + } catch (error) { + console.error('Failed to delete user', error); + alert('삭제 실패'); + } + }; + + const formatPhoneNumber = (value: string) => { + const cleaned = value.replace(/\D/g, ''); + if (cleaned.length <= 3) { + return cleaned; + } else if (cleaned.length <= 7) { + return `${cleaned.slice(0, 3)}-${cleaned.slice(3)}`; + } else { + return `${cleaned.slice(0, 3)}-${cleaned.slice(3, 7)}-${cleaned.slice(7, 11)}`; + } + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + try { + if (isEditing) { + // Update + const payload: any = { ...formData }; + if (!payload.password) delete payload.password; // Don't send empty password + + await apiClient.put(`/users/${formData.id}`, payload); + alert('수정되었습니다.'); + } else { + // Create + if (!formData.password) return alert('비밀번호를 입력하세요.'); + await apiClient.post('/users', formData); + alert('등록되었습니다.'); + } + setIsModalOpen(false); + fetchUsers(); + } catch (error: any) { + console.error('Submit failed', error); + const errorMsg = error.response?.data?.error || error.message || '저장 중 오류가 발생했습니다.'; + alert(`오류: ${errorMsg}`); + } + }; + + return ( +
+
+
+

시스템 관리 - 사용자 관리

+

시스템 접속 권한 및 사용자 정보를 관리합니다.

+
+ +
+ + +
+ + + + + + + + + + + + + {users.map((user) => ( + + + + + + + + + ))} + {users.length === 0 && !loading && ( + + + + )} + +
아이디 / 권한이름소속 / 직위연락처마지막 접속관리
+
{user.id}
+ + {user.role === 'admin' ? : } + {user.role === 'admin' ? '관리자' : '사용자'} + +
{user.name} +
{user.department || '-'}
+
{user.position}
+
{user.phone || '-'} + {user.last_login ? new Date(user.last_login).toLocaleString() : '접속 기록 없음'} + +
+ + +
+
등록된 사용자가 없습니다.
+
+
+ + {/* Modal */} + {isModalOpen && ( +
+ +
+

+ {isEditing ? '사용자 정보 수정' : '새 사용자 등록'} +

+ +
+
+
+ + setFormData({ ...formData, id: e.target.value })} + disabled={isEditing} // ID cannot be changed on edit + placeholder="로그인 아이디" + required + autoComplete="off" + /> +
+ +
+ + setFormData({ ...formData, password: e.target.value })} + placeholder={isEditing ? "(변경시에만 입력)" : "비밀번호"} + required={!isEditing} + autoComplete="new-password" + /> +
+ +
+
+ + setFormData({ ...formData, name: e.target.value })} + required + /> +
+
+ + +
+
+ +
+
+ + setFormData({ ...formData, department: e.target.value })} + /> +
+
+ + setFormData({ ...formData, position: e.target.value })} + /> +
+
+ +
+ + setFormData({ ...formData, phone: formatPhoneNumber(e.target.value) })} + placeholder="예: 010-1234-5678" + maxLength={13} + /> +
+ +
+ + +
+
+
+
+ )} +
+ ); +} diff --git a/src/production/pages/ProductionPage.tsx b/src/production/pages/ProductionPage.tsx new file mode 100644 index 0000000..4963cf1 --- /dev/null +++ b/src/production/pages/ProductionPage.tsx @@ -0,0 +1,12 @@ + + +export function ProductionPage() { + return ( +
+

생산 관리

+
+

생산 관리 대시보드입니다.

+
+
+ ); +} diff --git a/src/shared/api/assetApi.ts b/src/shared/api/assetApi.ts new file mode 100644 index 0000000..47cde10 --- /dev/null +++ b/src/shared/api/assetApi.ts @@ -0,0 +1,150 @@ +import { apiClient } from './client'; + +// Frontend Interface (CamelCase) +export interface Asset { + id: string; + name: string; + category: string; + model: string; // DB: model_name + serialNumber: string; // DB: serial_number + manufacturer: string; + location: string; + purchaseDate: string; // DB: purchase_date + manager: string; + status: 'active' | 'maintain' | 'broken' | 'disposed'; + purchasePrice?: number; // DB: purchase_price + image?: string; // DB: image_url + calibrationCycle?: string; // DB: calibration_cycle + specs?: string; +} + +// DB Interface (SnakeCase) - for internal mapping +interface DBAsset { + id: string; + name: string; + category: string; + model_name: string; + serial_number: string; + manufacturer: string; + location: string; + purchase_date: string; + manager: string; + status: 'active' | 'maintain' | 'broken' | 'disposed'; + purchase_price?: number; + image_url?: string; + calibration_cycle: string; + specs: string; + created_at: string; + updated_at: string; +} + +export interface MaintenanceRecord { + id: number; + asset_id: string; + maintenance_date: string; + type: string; + content: string; + image_url?: string; // Legacy + images?: string[]; // New: Multiple images + created_at?: string; +} + +// Manual Interface +export interface Manual { + id: number; + asset_id: string; + file_name: string; + file_url: string; + created_at?: string; +} + +export const assetApi = { + getAll: async (): Promise => { + const response = await apiClient.get('/assets'); + return response.data.map(mapDBToAsset); + }, + + getById: async (id: string): Promise => { + const response = await apiClient.get(`/assets/${id}`); + return mapDBToAsset(response.data); + }, + + create: (data: Partial) => { + const dbData = mapAssetToDB(data); + return apiClient.post<{ message: string, id: string }>('/assets', dbData); + }, + + update: (id: string, data: Partial) => { + const dbData = mapAssetToDB(data); + return apiClient.put(`/assets/${id}`, dbData); + }, + + uploadImage: (file: File) => { + const formData = new FormData(); + formData.append('image', file); + return apiClient.post<{ url: string }>('/upload', formData, { + headers: { 'Content-Type': 'multipart/form-data' } + }); + }, + + // Maintenance History + getMaintenanceHistory: async (assetId: string): Promise => { + const response = await apiClient.get(`/assets/${assetId}/maintenance`); + return response.data.map(record => ({ + ...record, + // Ensure images is parsed as array if it comes as string from DB + images: typeof record.images === 'string' ? JSON.parse(record.images) : (record.images || []) + })); + }, + + addMaintenance: async (assetId: string, data: Partial) => { + return apiClient.post(`/assets/${assetId}/maintenance`, data); + }, + + deleteMaintenance: async (id: number) => { + return apiClient.delete(`/maintenance/${id}`); + }, + + // Manuals / Instructions + getManuals: async (assetId: string): Promise => { + const response = await apiClient.get(`/assets/${assetId}/manuals`); + return response.data; + }, + + addManual: async (assetId: string, data: { file_name: string; file_url: string }) => { + return apiClient.post(`/assets/${assetId}/manuals`, data); + }, + + deleteManual: async (id: number) => { + return apiClient.delete(`/manuals/${id}`); + } +}; + +// Helper Mappers +const mapDBToAsset = (db: DBAsset): Asset => ({ + id: db.id, + name: db.name, + category: db.category, + model: db.model_name || '', + serialNumber: db.serial_number || '', + manufacturer: db.manufacturer || '', + location: db.location || '', + purchaseDate: db.purchase_date ? new Date(db.purchase_date).toISOString().split('T')[0] : '', // Format YYYY-MM-DD + manager: db.manager || '', + status: db.status, + purchasePrice: db.purchase_price, + image: db.image_url, + calibrationCycle: db.calibration_cycle || '', + specs: db.specs || '' +}); + +const mapAssetToDB = (asset: Partial): Partial => { + const db: any = { ...asset }; + if (asset.model !== undefined) { db.model_name = asset.model; delete db.model; } + if (asset.serialNumber !== undefined) { db.serial_number = asset.serialNumber; delete db.serialNumber; } + if (asset.purchaseDate !== undefined) { db.purchase_date = asset.purchaseDate; delete db.purchaseDate; } + if (asset.purchasePrice !== undefined) { db.purchase_price = asset.purchasePrice; delete db.purchasePrice; } + if (asset.image !== undefined) { db.image_url = asset.image; delete db.image; } + if (asset.calibrationCycle !== undefined) { db.calibration_cycle = asset.calibrationCycle; delete db.calibrationCycle; } + return db; +}; diff --git a/src/shared/api/client.ts b/src/shared/api/client.ts new file mode 100644 index 0000000..efc62f4 --- /dev/null +++ b/src/shared/api/client.ts @@ -0,0 +1,27 @@ +import axios from 'axios'; + +// Backend URL +// Empty string for relative path (works for both Dev via Proxy and Prod) +export const SERVER_URL = ''; +export const BASE_URL = '/api'; + +export const apiClient = axios.create({ + baseURL: BASE_URL, + headers: { + 'Content-Type': 'application/json', + }, + withCredentials: true // Enable sending/receiving cookies +}); + +let csrfToken: string | null = null; + +export const setCsrfToken = (token: string | null) => { + csrfToken = token; +}; + +apiClient.interceptors.request.use((config) => { + if (csrfToken && config.method && config.method.toUpperCase() !== 'GET' && config.method.toUpperCase() !== 'HEAD') { + config.headers['X-CSRF-Token'] = csrfToken; + } + return config; +}); diff --git a/src/shared/auth/AuthContext.tsx b/src/shared/auth/AuthContext.tsx new file mode 100644 index 0000000..690f7c4 --- /dev/null +++ b/src/shared/auth/AuthContext.tsx @@ -0,0 +1,89 @@ +import { createContext, useContext, useState, type ReactNode, useEffect } from 'react'; +import { apiClient, setCsrfToken } from '../api/client'; + +export interface User { + id: string; + name: string; + role: 'admin' | 'user'; + department?: string; + position?: string; + phone?: string; + last_login?: string; +} + +interface AuthContextType { + user: User | null; + login: (id: string, password: string) => Promise; + logout: () => void; + isAuthenticated: boolean; + isLoading: boolean; +} + +const AuthContext = createContext(null); + +export function AuthProvider({ children }: { children: ReactNode }) { + const [user, setUser] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + // Check for existing session on mount + useEffect(() => { + const checkSession = async () => { + try { + const response = await apiClient.get('/check'); + if (response.data.isAuthenticated) { + setUser(response.data.user); + if (response.data.csrfToken) { + setCsrfToken(response.data.csrfToken); + } + } + } catch (error) { + console.error('Session check failed:', error); + } finally { + setIsLoading(false); + } + }; + checkSession(); + }, []); + + const login = async (id: string, password: string) => { + try { + const response = await apiClient.post('/login', { id, password }); + if (response.data.success) { + setUser(response.data.user); + if (response.data.csrfToken) { + setCsrfToken(response.data.csrfToken); + } + return true; + } + return false; + } catch (error) { + console.error('Login failed:', error); + return false; + } + }; + + const logout = async () => { + try { + await apiClient.post('/logout'); + } catch (error) { + console.error('Logout failed:', error); + } finally { + setUser(null); + setCsrfToken(null); + } + }; + + return ( + + {children} + + ); +} + +export function useAuth() { + const context = useContext(AuthContext); + if (!context) { + throw new Error('useAuth must be used within an AuthProvider'); + } + return context; +} diff --git a/src/shared/auth/AuthGuard.tsx b/src/shared/auth/AuthGuard.tsx new file mode 100644 index 0000000..2260f84 --- /dev/null +++ b/src/shared/auth/AuthGuard.tsx @@ -0,0 +1,20 @@ +import type { ReactNode } from 'react'; +import { Navigate, useLocation } from 'react-router-dom'; +import { useAuth } from './AuthContext'; + +export function AuthGuard({ children }: { children: ReactNode }) { + const { isAuthenticated, isLoading } = useAuth(); + const location = useLocation(); + + if (isLoading) { + // Simple loading state, could be a spinner + return
Loading...
; + } + + if (!isAuthenticated) { + // Redirect to the login page, but save the current location they were trying to go to + return ; + } + + return children; +} diff --git a/src/shared/context/SystemContext.tsx b/src/shared/context/SystemContext.tsx new file mode 100644 index 0000000..72ed3aa --- /dev/null +++ b/src/shared/context/SystemContext.tsx @@ -0,0 +1,63 @@ +import { createContext, useContext, useState, useEffect, type ReactNode } from 'react'; +import { apiClient } from '../api/client'; +import { useAuth } from '../auth/AuthContext'; + +export interface ModuleState { + active: boolean; + type: 'dev' | 'sub' | 'demo' | null; + expiry: string | null; +} + +interface SystemContextType { + modules: Record; + refreshModules: () => Promise; + isLoading: boolean; +} + +const SystemContext = createContext(null); + +export function SystemProvider({ children }: { children: ReactNode }) { + const [modules, setModules] = useState>({}); + const [isLoading, setIsLoading] = useState(true); + const { isAuthenticated } = useAuth(); // Only load if authenticated + + const refreshModules = async () => { + try { + const response = await apiClient.get('/system/modules'); + if (response.data.modules) { + setModules(response.data.modules); + } else { + // Fallback or assume old format (unsafe with new backend) + // Better to assume new format based on my changes + setModules(response.data.modules || response.data); + } + } catch (error) { + console.error('Failed to load system modules:', error); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + if (isAuthenticated) { + refreshModules(); + } else { + setModules({}); + setIsLoading(false); + } + }, [isAuthenticated]); + + return ( + + {children} + + ); +} + +export function useSystem() { + const context = useContext(SystemContext); + if (!context) { + throw new Error('useSystem must be used within a SystemProvider'); + } + return context; +} diff --git a/src/shared/ui/Button.tsx b/src/shared/ui/Button.tsx new file mode 100644 index 0000000..19e294f --- /dev/null +++ b/src/shared/ui/Button.tsx @@ -0,0 +1,31 @@ +import { type ButtonHTMLAttributes, type ReactNode } from 'react'; +import './Components.css'; + +interface ButtonProps extends ButtonHTMLAttributes { + children: ReactNode; + variant?: 'primary' | 'secondary' | 'danger' | 'ghost'; + size?: 'sm' | 'md' | 'lg'; + icon?: ReactNode; +} + +export function Button({ + children, + variant = 'primary', + size = 'md', + icon, + className = '', + ...props +}: ButtonProps) { + const variantClass = `ui-btn-${variant}`; + const sizeClass = `ui-btn-${size}`; + + return ( + + ); +} diff --git a/src/shared/ui/Card.tsx b/src/shared/ui/Card.tsx new file mode 100644 index 0000000..d18d298 --- /dev/null +++ b/src/shared/ui/Card.tsx @@ -0,0 +1,24 @@ +import type { ReactNode } from 'react'; + +interface CardProps { + children: ReactNode; + className?: string; + title?: string; + action?: ReactNode; +} + +export function Card({ children, className = '', title, action }: CardProps) { + return ( +
+ {(title || action) && ( +
+ {title &&

{title}

} + {action &&
{action}
} +
+ )} +
+ {children} +
+
+ ); +} diff --git a/src/shared/ui/Components.css b/src/shared/ui/Components.css new file mode 100644 index 0000000..48260a4 --- /dev/null +++ b/src/shared/ui/Components.css @@ -0,0 +1,173 @@ +/* Shared UI Components Styles */ + +/* Base reset for consistency */ +.ui-base { + box-sizing: border-box; + font-family: var(--font-sans); +} + +/* --- Button --- */ +.ui-btn { + display: inline-flex; + align-items: center; + justify-content: center; + font-weight: 500; + border-radius: var(--radius-md); + cursor: pointer; + transition: all 0.2s ease-in-out; + border: 1px solid transparent; + outline: none; + white-space: nowrap; +} + +.ui-btn:focus-visible { + box-shadow: 0 0 0 2px var(--color-brand-accent), 0 0 0 4px var(--color-brand-primary); +} + +/* Variants */ +.ui-btn-primary { + background-color: var(--color-bg-sidebar); + /* Slate 800 */ + color: white; +} + +.ui-btn-primary:hover { + background-color: #0f172a; + /* Slate 900 */ +} + +.ui-btn-secondary { + background-color: white; + color: var(--color-text-primary); + border-color: var(--color-border); + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); +} + +.ui-btn-secondary:hover { + background-color: #f8fafc; + /* Slate 50 */ +} + +.ui-btn-danger { + background-color: #dc2626; + /* Red 600 */ + color: white; +} + +.ui-btn-danger:hover { + background-color: #b91c1c; + /* Red 700 */ +} + +.ui-btn-ghost { + background-color: transparent; + color: var(--color-text-secondary); +} + +.ui-btn-ghost:hover { + background-color: #f1f5f9; + /* Slate 100 */ + color: var(--color-text-primary); +} + +/* Sizes */ +.ui-btn-sm { + padding: 0.375rem 0.75rem; + font-size: 0.875rem; +} + +.ui-btn-md { + padding: 0.625rem 1.25rem; + font-size: 0.95rem; + /* readable base size */ +} + +.ui-btn-lg { + padding: 0.875rem 1.75rem; + font-size: 1.125rem; +} + + +/* --- Input & Select Container --- */ +.ui-field-container { + width: 100%; + margin-bottom: 0.5rem; +} + +.ui-label { + display: block; + font-size: 1rem; + /* Base size */ + font-weight: 600; + color: var(--color-text-primary); + margin-bottom: 0.5rem; +} + +.ui-input-wrapper { + position: relative; + width: 100%; +} + +.ui-icon-wrapper { + position: absolute; + top: 50%; + transform: translateY(-50%); + left: 1rem; + color: var(--color-text-secondary); + pointer-events: none; + display: flex; + align-items: center; +} + +/* Common form element styles */ +.ui-input, +.ui-select { + display: block; + width: 100%; + padding: 0.75rem 1rem; + background-color: white; + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + color: var(--color-text-primary); + font-size: 1rem; + /* Clear readable text */ + line-height: 1.5; + transition: var(--transition-base); + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); +} + +.ui-input:focus, +.ui-select:focus { + outline: none; + border-color: var(--color-brand-primary); + box-shadow: 0 0 0 3px rgba(14, 165, 233, 0.15); + /* Ring effect */ +} + +.ui-input:disabled, +.ui-select:disabled { + background-color: #f8fafc; + color: #94a3b8; + cursor: not-allowed; +} + +.ui-input.has-icon { + padding-left: 2.75rem; + /* Space for icon */ +} + +.ui-input.error, +.ui-select.error { + border-color: #ef4444; +} + +.ui-input.error:focus, +.ui-select.error:focus { + box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.15); +} + +.ui-error-msg { + margin-top: 0.375rem; + font-size: 0.875rem; + color: #ef4444; +} \ No newline at end of file diff --git a/src/shared/ui/Input.tsx b/src/shared/ui/Input.tsx new file mode 100644 index 0000000..33ad3df --- /dev/null +++ b/src/shared/ui/Input.tsx @@ -0,0 +1,37 @@ +import { type InputHTMLAttributes, forwardRef, type ReactNode } from 'react'; +import './Components.css'; + +interface InputProps extends InputHTMLAttributes { + label?: string; + error?: string; + icon?: ReactNode; +} + +export const Input = forwardRef( + ({ label, error, icon, className = '', ...props }, ref) => { + return ( +
+ {label && ( + + )} +
+ {icon && ( +
+ {icon} +
+ )} + +
+ {error &&

{error}

} +
+ ); + } +); + +Input.displayName = 'Input'; diff --git a/src/shared/ui/Select.tsx b/src/shared/ui/Select.tsx new file mode 100644 index 0000000..b6df871 --- /dev/null +++ b/src/shared/ui/Select.tsx @@ -0,0 +1,40 @@ +import { type SelectHTMLAttributes, forwardRef } from 'react'; +import './Components.css'; + +interface SelectProps extends SelectHTMLAttributes { + label?: string; + error?: string; + options: { label: string; value: string }[]; + placeholder?: string; +} + +export const Select = forwardRef( + ({ label, error, options, placeholder, className = '', ...props }, ref) => { + return ( +
+ {label && ( + + )} +
+ +
+ {error &&

{error}

} +
+ ); + } +); + +Select.displayName = 'Select'; diff --git a/src/system/pages/LicensePage.tsx b/src/system/pages/LicensePage.tsx new file mode 100644 index 0000000..bcd2336 --- /dev/null +++ b/src/system/pages/LicensePage.tsx @@ -0,0 +1,314 @@ +import { useState } from 'react'; +import { useSystem } from '../../shared/context/SystemContext'; +import { apiClient } from '../../shared/api/client'; +import { Check, X, Key, Shield, AlertTriangle, Terminal } from 'lucide-react'; + +export function LicensePage() { + const { modules, refreshModules } = useSystem(); + const [selectedModule, setSelectedModule] = useState(null); + const [licenseKey, setLicenseKey] = useState(''); + const [isSubmitting, setIsSubmitting] = useState(false); + const [error, setError] = useState(null); + + const moduleInfo: Record = { + 'asset': { title: '자산 관리 모듈', desc: '자산 등록, 조회, 수정 및 유지보수 이력 관리' }, + 'production': { title: '생산 관리 모듈', desc: '생산 계획, 실적 및 공정 관리' }, + 'monitoring': { title: 'CCTV 모듈', desc: '실시간 영상 모니터링 및 녹화 관리' } + }; + + // Subscriber Configuration State + const [serverSubscriberId, setServerSubscriberId] = useState(''); + const [inputSubscriberId, setInputSubscriberId] = useState(''); + const [isConfigSaving, setIsConfigSaving] = useState(false); + + // Initial Load for Subscriber ID + useState(() => { + fetchSubscriberId(); + }); + + async function fetchSubscriberId() { + try { + const res = await apiClient.get('/system/config'); + setServerSubscriberId(res.data.subscriber_id || ''); + setInputSubscriberId(res.data.subscriber_id || ''); + } catch (err) { + console.error('Failed to load subscriber config', err); + } + } + + const handleSaveConfig = async () => { + if (!inputSubscriberId.trim()) return alert('구독자 ID를 입력해주세요.'); + setIsConfigSaving(true); + try { + await apiClient.post('/system/config', { subscriber_id: inputSubscriberId }); + setServerSubscriberId(inputSubscriberId); + alert('구독자 ID가 저장되었습니다.'); + } catch (err) { + alert('설정 저장 실패'); + } finally { + setIsConfigSaving(false); + } + }; + + const handleActivate = async (e: React.FormEvent) => { + e.preventDefault(); + if (!selectedModule || !licenseKey.trim()) return; + + if (!serverSubscriberId) { + setError('서버 구독자 ID가 설정되지 않았습니다. 먼저 설정을 완료해주세요.'); + return; + } + + setIsSubmitting(true); + setError(null); + + try { + // Include user context if needed, but module activation is system-wide + await apiClient.post(`/system/modules/${selectedModule}/activate`, { licenseKey }); + await refreshModules(); + setSelectedModule(null); + setLicenseKey(''); + alert('모듈이 성공적으로 활성화되었습니다.'); + } catch (err: any) { + setError(err.response?.data?.error || '활성화 실패'); + } finally { + setIsSubmitting(false); + } + }; + + const handleDeactivate = async (code: string) => { + if (!confirm('정말 이 모듈을 비활성화 하시겠습니까? 사용자 메뉴에서 즉시 사라집니다.')) return; + try { + await apiClient.post(`/system/modules/${code}/deactivate`); + await refreshModules(); + } catch (err) { + alert('비활성화 실패'); + } + }; + + // ... Generator logic omitted for brevity as it's dev-only ... + + // History State + const [historyModule, setHistoryModule] = useState(null); + const [historyData, setHistoryData] = useState([]); + + const fetchHistory = async (code: string) => { + try { + const res = await apiClient.get(`/system/modules/${code}/history`); + setHistoryData(res.data); + setHistoryModule(code); + } catch (err) { + alert('이력을 불러오는데 실패했습니다.'); + } + }; + + return ( +
+

+ + 라이선스 및 모듈 관리 +

+ + {/* Server Configuration Section */} +
+

+ + 서버 환경 설정 +

+
+
+ + setInputSubscriberId(e.target.value)} + /> +

+ * 라이선스 키에 포함된 ID와 일치해야 활성화됩니다. +

+
+ +
+
+ +
+ {Object.entries(moduleInfo).map(([code, info]) => { + // Check typing for modules[code] + const state = modules[code] as any || { active: false, type: null, expiry: null }; + const isActive = state.active; + + return ( +
+
+

{info.title}

+ {isActive ? ( + + 활성 + + ) : ( + + 비활성 + + )} +
+

{info.desc}

+ + {isActive && ( +
+
+ 유형: + {state.type === 'dev' ? '개발자용' : state.type === 'sub' ? '구독자용' : '데모용'} +
+
+ 구독자: + {state.subscriber_id || 'N/A'} +
+
+ 만료일: + + {state.expiry ? new Date(state.expiry).toLocaleDateString() : '무제한'} + +
+
+ )} + +
+ {!isActive ? ( + + ) : ( +
+ + +
+ )} +
+
+ ); + })} +
+ + {/* License Activation Modal */} + {selectedModule && ( +
+
+

라이선스 키 입력

+

+ 발급받은 라이선스 키를 입력하여 {moduleInfo[selectedModule]?.title}을(를) 활성화하세요. +

+ +
+