diff --git a/.env b/.env deleted file mode 100644 index 47bf3a1..0000000 --- a/.env +++ /dev/null @@ -1,9 +0,0 @@ -#Account Database configuration for PostgreSQL -POSTGRES_USER=postgres -POSTGRES_PASSWORD=postgres -POSTGRES_DB=ndms_db - -#Account Minio S3 configuration -MINIO_ROOT_USER=minioadmin -MINIO_ROOT_PASSWORD=minioadmin -MINIO_BUCKET_NAME=ndms-bucket \ No newline at end of file diff --git a/.gitignore b/.gitignore index 54f8001..1739879 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,7 @@ __pycache__/ .Python venv/ .venv/ -env/ +.env ENV/ # Node.js diff --git a/PHẦN MỀM QUẢN LÝ THIẾT BỊ MẠNG (1) copy.text b/PHẦN MỀM QUẢN LÝ THIẾT BỊ MẠNG (1) copy.text new file mode 100644 index 0000000..906f3dd Binary files /dev/null and b/PHẦN MỀM QUẢN LÝ THIẾT BỊ MẠNG (1) copy.text differ diff --git a/backend/Dockerfile b/backend/ scheduler/_init_.py similarity index 100% rename from backend/Dockerfile rename to backend/ scheduler/_init_.py diff --git a/backend/ scheduler/alert_worker.py b/backend/ scheduler/alert_worker.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/ scheduler/ping_worker.py b/backend/ scheduler/ping_worker.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/ scheduler/scheduler.py b/backend/ scheduler/scheduler.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/ scheduler/snmp_worker.py b/backend/ scheduler/snmp_worker.py new file mode 100644 index 0000000..e69de29 diff --git a/.env.example b/backend/.env.example similarity index 100% rename from .env.example rename to backend/.env.example diff --git a/backend/app.py b/backend/app.py index 80c3013..06b81a1 100644 --- a/backend/app.py +++ b/backend/app.py @@ -1,26 +1,30 @@ -from flask import Flask, jsonify -# from flask_cors import CORS -import os -from dotenv import load_dotenv +from flask import Flask -load_dotenv() # Đọc file .env +from common.exceptions.handler import ( + register_error_handlers +) -app = Flask(__name__) # ← Đây là dòng quan trọng nhất -# Cho phép React (localhost:5173) gọi API từ backend -CORS(app, origins=["http://localhost:5173", "http://127.0.0.1:5173"]) +from modules.device_type.routes import ( + device_type_bp +) -@app.route('/') -def home(): - return jsonify({ - "message": "✅ Backend NDMS đang chạy thành công!", - "status": "ok" - }) +app = Flask(__name__) -@app.route('/health') -def health(): - return jsonify({"status": "healthy"}) +# Register Global Exception Handlers +register_error_handlers(app) -# Phần này bạn hay dùng -if __name__ == '__main__': - app.run(host='0.0.0.0', port=8000, debug=True) \ No newline at end of file +# Register Blueprints +app.register_blueprint( + device_type_bp, + url_prefix="/api" +) + +# @app.route("/") +# def home(): +# return { +# "message": "NDMS Backend Running" +# } + +if __name__ == "__main__": + app.run(debug=True) \ No newline at end of file diff --git a/backend/common/constants/status_code.py b/backend/common/constants/status_code.py new file mode 100644 index 0000000..92ca199 --- /dev/null +++ b/backend/common/constants/status_code.py @@ -0,0 +1,9 @@ +HTTP_OK = 200 +HTTP_CREATED = 201 +HTTP_BAD_REQUEST = 400 +HTTP_UNAUTHORIZED = 401 +HTTP_FORBIDDEN = 403 +HTTP_NOT_FOUND = 404 +HTTP_CONFLICT = 409 +HTTP_UNPROCESSABLE_ENTITY = 422 +HTTP_INTERNAL_SERVER_ERROR = 500 \ No newline at end of file diff --git a/backend/common/exceptions/app_exception.py b/backend/common/exceptions/app_exception.py new file mode 100644 index 0000000..65c07d2 --- /dev/null +++ b/backend/common/exceptions/app_exception.py @@ -0,0 +1,62 @@ +# Đây là exception chung cho toàn bộ dự án +class AppException(Exception): # Tạo ra class AppException kế thừa lại Exception của thư viện + def __init__( + self, + message, + status_code=400, + error_code="BAD_REQUEST", + payload=None + ): + super().__init__(message) #kế thừa thêm thuộc tính + self.message = message + self.status_code = status_code + self.error_code = error_code + self.payload = payload or {} + + def to_dict(self): + return { + "success": False, + "error": self.error_code, + "message": self.message, + "details": self.payload + } + +# Rồi từ thằng kế thừa lại kế thừa tiếp từ thằng AppException +class BadRequestException(AppException): + def __init__(self, message="Bad request", payload=None): + super().__init__( + message=message, + status_code=400, + error_code="BAD_REQUEST", + payload=payload + ) + + +class NotFoundException(AppException): + def __init__(self, message="Resource not found", payload=None): + super().__init__( + message=message, + status_code=404, + error_code="NOT_FOUND", + payload=payload + ) + + +class ConflictException(AppException): + def __init__(self, message="Resource already exists", payload=None): + super().__init__( + message=message, + status_code=409, + error_code="CONFLICT", + payload=payload + ) + + +class UnprocessableEntityException(AppException): + def __init__(self, message="Unprocessable entity", payload=None): + super().__init__( + message=message, + status_code=422, + error_code="UNPROCESSABLE_ENTITY", + payload=payload + ) \ No newline at end of file diff --git a/backend/common/exceptions/handler.py b/backend/common/exceptions/handler.py new file mode 100644 index 0000000..a504320 --- /dev/null +++ b/backend/common/exceptions/handler.py @@ -0,0 +1,50 @@ +from marshmallow import ValidationError + +from common.exceptions.app_exception import AppException +from common.response.api_response import error_response +from common.constants.status_code import ( + HTTP_BAD_REQUEST, + HTTP_NOT_FOUND, + HTTP_INTERNAL_SERVER_ERROR +) +# Phải tạo global error handler(ở project cũ là để phần này ra app.py nhưng bây giờ chỉ cần tạo file hander rồi đăng kí ở app.py để nó thành global bắt lỗi toàn dự án) +# Ngoài ra để có được các lỗi của toàn dự án, thì phải custom exception theo dự án bằng cách kế thừa lại Exception để custom thêm thuộc tính mới + +def register_error_handlers(app): # Đăng ký hàm này ở app.py để nó bắt lỗi toàn dự án + + @app.errorhandler(AppException) + def handle_app_exception(error): + return error_response( + message=error.message, + error=error.error_code, + status_code=error.status_code, + details=getattr(error, "payload", None) + ) + + + @app.errorhandler(ValidationError) + def handle_validation_error(error): + return error_response( + message="Invalid request data", + error="VALIDATION_ERROR", + status_code=HTTP_BAD_REQUEST, + details=error.messages + ) + + + @app.errorhandler(404) + def handle_not_found(error): + return error_response( + message="Route not found", + error="NOT_FOUND", + status_code=HTTP_NOT_FOUND + ) + + + @app.errorhandler(500) + def handle_internal_error(error): + return error_response( + message="Internal server error", + error="INTERNAL_SERVER_ERROR", + status_code=HTTP_INTERNAL_SERVER_ERROR + ) \ No newline at end of file diff --git a/backend/common/response/api_response.py b/backend/common/response/api_response.py new file mode 100644 index 0000000..5ff9b05 --- /dev/null +++ b/backend/common/response/api_response.py @@ -0,0 +1,28 @@ +from flask import jsonify +from common.constants.status_code import HTTP_OK, HTTP_BAD_REQUEST + + +def success_response( + data=None, + message="Success", + status_code=HTTP_OK +): + return jsonify({ + "success": True, + "message": message, + "data": data + }), status_code + + +def error_response( + message="Error", + error="ERROR", + status_code=HTTP_BAD_REQUEST, + details=None +): + return jsonify({ + "success": False, + "error": error, + "message": message, + "details": details + }), status_code \ No newline at end of file diff --git a/backend/config/database.py b/backend/config/database.py new file mode 100644 index 0000000..5995edb --- /dev/null +++ b/backend/config/database.py @@ -0,0 +1,25 @@ +#Connection to Database +from psycopg2.pool import SimpleConnectionPool +import os +from dotenv import load_dotenv + +load_dotenv() + +db_pool = SimpleConnectionPool( + minconn=1, + maxconn=10, + host=os.getenv("DB_HOST"), + port=os.getenv("DB_PORT"), + database=os.getenv("POSTGRES_DB"), + user=os.getenv("POSTGRES_USER"), + password=os.getenv("POSTGRES_PASSWORD") +) +# Tạo ra 10 conection khác nhau, mỗi lần dùng thì vào đây lấy 1 connection ra => Nó tạo sẵn một nhóm connection. Khi cần thì lấy ra, dùng xong trả lại. +# khác với kiểu cũ là mỗi lần request khác nhau lại phải tạo mới connection nó sẽ bị nặng và lâu hơn + + +def get_connection(): + return db_pool.getconn() # Lấy ra connection trong này + +def release_connection(conn): + db_pool.putconn(conn) # trả connection lại pool này \ No newline at end of file diff --git a/backend/database/init.sql b/backend/database/init.sql new file mode 100644 index 0000000..e69de29 diff --git a/backend/database/schema.sql b/backend/database/schema.sql new file mode 100644 index 0000000..aba2382 --- /dev/null +++ b/backend/database/schema.sql @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/backend/database/seed.sql b/backend/database/seed.sql new file mode 100644 index 0000000..e69de29 diff --git a/backend/modules/device_type/controller.py b/backend/modules/device_type/controller.py new file mode 100644 index 0000000..350a951 --- /dev/null +++ b/backend/modules/device_type/controller.py @@ -0,0 +1,83 @@ +# ============================================ +# CONTROLLER: Nhận request → Validate → Gọi Service → Trả response +# ============================================ +# Controller KHÔNG chứa business logic (check trùng tên, tính toán...) +# Controller CHỈ làm 3 việc: +# 1. Lấy dữ liệu từ request (body, params, query string) +# 2. Validate dữ liệu (bằng Marshmallow schema) +# 3. Gọi service → trả response +# ============================================ + +from flask import request +from common.response.api_response import success_response +from common.constants.status_code import HTTP_CREATED +from modules.device_type.service import ( + get_device_types_service, + create_device_type_service +) +from modules.device_type.schemas import CreateDeviceTypeSchema + + +def get_device_types(): + """GET /api/device-types — Lấy danh sách tất cả""" + device_types = get_device_types_service() + return success_response( + data=device_types, + message="Device types retrieved successfully" + ) + + +def get_device_type_by_id(device_type_id): + """GET /api/device-types/ — Lấy chi tiết theo ID""" + # TODO: Triển khai sau + pass + + +def create_device_type(): + """ + POST /api/device-types — Tạo mới device type + + Luồng xử lý: + 1. Lấy JSON body từ request + 2. Validate bằng Marshmallow schema + - Nếu sai → Marshmallow tự throw ValidationError + - Global handler bắt → trả 400 kèm chi tiết lỗi + 3. Gọi service (service sẽ check trùng tên + insert DB) + 4. Trả 201 Created + data vừa tạo + """ + # Bước 1: Lấy JSON body từ request + body = request.get_json() + # request.get_json() parse body thành dict Python + # VD: {"name": "Router", "color": "#3B82F6"} → {'name': 'Router', 'color': '#3B82F6'} + + # Bước 2: Validate bằng schema + schema = CreateDeviceTypeSchema() + data = schema.load(body) + # schema.load() làm 2 việc: + # a) Validate: check required, type, regex... → nếu sai throw ValidationError + # b) Deserialize: loại bỏ field thừa, áp dụng load_default + # VD: body = {"name": "Router", "color": "#3B82F6", "hack": "xss"} + # → data = {"name": "Router", "color": "#3B82F6", "sort_order": 0, "is_active": True} + # → "hack" bị loại bỏ, sort_order/is_active được thêm default + + # Bước 3: Gọi service + new_device_type = create_device_type_service(data) + + # Bước 4: Trả response 201 Created + return success_response( + data=new_device_type, + message="Device type created successfully", + status_code=HTTP_CREATED # 201 thay vì 200 — đúng chuẩn REST cho POST thành công + ) + + +def update_device_type(device_type_id): + """PUT /api/device-types/ — Cập nhật""" + # TODO: Triển khai sau + pass + + +def delete_device_type(device_type_id): + """DELETE /api/device-types/ — Xóa""" + # TODO: Triển khai sau + pass \ No newline at end of file diff --git a/backend/modules/device_type/exceptions.py b/backend/modules/device_type/exceptions.py new file mode 100644 index 0000000..cac8a54 --- /dev/null +++ b/backend/modules/device_type/exceptions.py @@ -0,0 +1,30 @@ +# Đây là exception cho riêng cho từng module +from common.exceptions.app_exception import ( + NotFoundException, + ConflictException, + BadRequestException +) + + +class DeviceTypeNotFoundException(NotFoundException): + def __init__(self, device_type_id): + super().__init__( + message=f"Device type not found with id={device_type_id}", + payload={"device_type_id": device_type_id} + ) + + +class DeviceTypeAlreadyExistsException(ConflictException): + def __init__(self, name): + super().__init__( + message=f"Device type already exists with name={name}", + payload={"name": name} + ) + + +class DeviceTypeInUseException(BadRequestException): + def __init__(self, device_type_id): + super().__init__( + message="Cannot delete device type because it is being used by devices", + payload={"device_type_id": device_type_id} + ) \ No newline at end of file diff --git a/backend/modules/device_type/repository.py b/backend/modules/device_type/repository.py new file mode 100644 index 0000000..d01f527 --- /dev/null +++ b/backend/modules/device_type/repository.py @@ -0,0 +1,120 @@ +from config.database import get_connection, release_connection + +# ============================================ +# Hàm chung: Chuyển 1 row (tuple) thành dict +# Tại sao tách riêng? Vì nhiều hàm đều cần chuyển row → dict, +# viết 1 lần dùng lại, tránh copy-paste +# ============================================ +def _row_to_dict(row): + return { + "id": str(row[0]), + "name": row[1], + "description": row[2], + "icon_url": row[3], + "color": row[4], + "sort_order": row[5], + "is_active": row[6], + "created": row[7].isoformat() if row[7] else None, + "modified": row[8].isoformat() if row[8] else None + } + + +# ============================================ +# GET ALL: Lấy danh sách tất cả device types +# ============================================ +def find_all_device_types(): + conn = get_connection() + cur = None + try: + cur = conn.cursor() + cur.execute(""" + SELECT id, name, description, icon_url, color, sort_order, is_active, created, modified + FROM device_types + ORDER BY sort_order ASC, name ASC + """) + + rows = cur.fetchall() + + device_types = [] + for row in rows: + device_types.append(_row_to_dict(row)) + + # ✅ FIX: return PHẢI nằm NGOÀI vòng for + # Trước đó return nằm trong for → chỉ trả về 1 row rồi dừng + return device_types + + finally: + if cur: + cur.close() + # ✅ FIX: PHẢI trả connection về pool, không thì sau 10 request app sẽ đứng + release_connection(conn) + + +# ============================================ +# FIND BY NAME: Tìm device type theo tên (để check trùng tên khi tạo mới) +# Trả về dict nếu tìm thấy, None nếu không +# ============================================ +def find_device_type_by_name(name): + conn = get_connection() + cur = None + try: + cur = conn.cursor() + # Dùng LOWER() để so sánh không phân biệt hoa/thường + # VD: "Router" và "router" coi như trùng + cur.execute(""" + SELECT id, name, description, icon_url, color, sort_order, is_active, created, modified + FROM device_types + WHERE LOWER(name) = LOWER(%s) + """, (name,)) + # ⚠️ LƯU Ý: (name,) có dấu phẩy → tạo tuple 1 phần tử + # Nếu viết (name) thì Python hiểu là string, không phải tuple → lỗi + + row = cur.fetchone() # fetchone() vì chỉ cần 1 kết quả + if row: + return _row_to_dict(row) + return None + + finally: + if cur: + cur.close() + release_connection(conn) + + +# ============================================ +# INSERT: Thêm mới 1 device type vào database +# Trả về dict của device type vừa tạo (có id, created, modified từ DB) +# ============================================ +def insert_device_type(data): + conn = get_connection() + cur = None + try: + cur = conn.cursor() + cur.execute(""" + INSERT INTO device_types (name, description, icon_url, color, sort_order, is_active) + VALUES (%s, %s, %s, %s, %s, %s) + RETURNING id, name, description, icon_url, color, sort_order, is_active, created, modified + """, ( + data["name"], + data.get("description"), # .get() trả về None nếu không có key + data.get("icon_url"), + data["color"], + data.get("sort_order", 0), # default = 0 nếu không truyền + data.get("is_active", True) # default = True nếu không truyền + )) + # RETURNING: PostgreSQL trả về row vừa INSERT (bao gồm id, created, modified do DB tự tạo) + # → Không cần SELECT lại sau khi INSERT + + row = cur.fetchone() + conn.commit() # ⚠️ QUAN TRỌNG: INSERT/UPDATE/DELETE phải commit() để lưu vào DB + # Nếu không commit → dữ liệu chỉ nằm trong transaction, khi connection đóng → mất hết + + return _row_to_dict(row) + + except Exception: + conn.rollback() # Nếu có lỗi → rollback để hủy transaction, tránh dữ liệu bẩn + raise # raise lại exception để tầng trên (service/controller) xử lý + + finally: + if cur: + cur.close() + release_connection(conn) \ No newline at end of file diff --git a/backend/modules/device_type/routes.py b/backend/modules/device_type/routes.py new file mode 100644 index 0000000..3ed18fd --- /dev/null +++ b/backend/modules/device_type/routes.py @@ -0,0 +1,21 @@ +from flask import Blueprint + +from modules.device_type.controller import ( + get_device_types, + get_device_type_by_id, + create_device_type, + update_device_type, + delete_device_type +) + +device_type_bp = Blueprint( + "device_type", + __name__, + url_prefix="/device-types" +) + +device_type_bp.route("", methods=["GET"])(get_device_types) +device_type_bp.route("/", methods=["GET"])(get_device_type_by_id) +device_type_bp.route("", methods=["POST"])(create_device_type) +device_type_bp.route("/", methods=["PUT"])(update_device_type) +device_type_bp.route("/", methods=["DELETE"])(delete_device_type) \ No newline at end of file diff --git a/backend/modules/device_type/schemas.py b/backend/modules/device_type/schemas.py new file mode 100644 index 0000000..11874c1 --- /dev/null +++ b/backend/modules/device_type/schemas.py @@ -0,0 +1,67 @@ +from marshmallow import Schema, fields, validate + +HEX_COLOR_REGEX = r"^#[0-9A-Fa-f]{6}$" + + +class CreateDeviceTypeSchema(Schema): + name = fields.String( + required=True, + validate=validate.Length(min=1, max=100) + ) + + description = fields.String( + required=False, + allow_none=True + ) + + icon_url = fields.String( + required=False, + allow_none=True, + validate=validate.Length(max=512) + ) + + color = fields.String( + required=True, + validate=validate.Regexp(HEX_COLOR_REGEX) + ) + + sort_order = fields.Integer( + required=False, + load_default=0 + ) + + is_active = fields.Boolean( + required=False, + load_default=True + ) + + +class UpdateDeviceTypeSchema(Schema): + name = fields.String( + required=False, + validate=validate.Length(min=1, max=100) + ) + + description = fields.String( + required=False, + allow_none=True + ) + + icon_url = fields.String( + required=False, + allow_none=True, + validate=validate.Length(max=512) + ) + + color = fields.String( + required=False, + validate=validate.Regexp(HEX_COLOR_REGEX) + ) + + sort_order = fields.Integer( + required=False + ) + + is_active = fields.Boolean( + required=False + ) \ No newline at end of file diff --git a/backend/modules/device_type/service.py b/backend/modules/device_type/service.py new file mode 100644 index 0000000..032b3b6 --- /dev/null +++ b/backend/modules/device_type/service.py @@ -0,0 +1,38 @@ +# ============================================ +# SERVICE LAYER: Chứa Business Logic +# ============================================ +# Controller chỉ nhận request + trả response +# Repository chỉ truy vấn DB +# Service ở giữa: chứa LOGIC NGHIỆP VỤ (validate, check điều kiện, tính toán) +# ============================================ + +from modules.device_type.repository import ( + find_all_device_types, + find_device_type_by_name, + insert_device_type +) +from modules.device_type.exceptions import DeviceTypeAlreadyExistsException + + +def get_device_types_service(): + """Lấy danh sách tất cả device types — hiện tại không có logic gì thêm""" + return find_all_device_types() + + +def create_device_type_service(data): + """ + Tạo mới device type. + Business logic: + 1. Check xem tên đã tồn tại chưa (UNIQUE constraint) + 2. Nếu trùng → throw exception + 3. Nếu không trùng → insert vào DB + """ + # Bước 1: Check trùng tên + existing = find_device_type_by_name(data["name"]) + if existing: + # Throw exception → global handler bắt → trả 409 Conflict + raise DeviceTypeAlreadyExistsException(data["name"]) + + # Bước 2: Insert vào DB + new_device_type = insert_device_type(data) + return new_device_type \ No newline at end of file diff --git a/backend/modules/device_type/validator.py b/backend/modules/device_type/validator.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/requirements.txt b/backend/requirements.txt index 7dd1ea4..3112071 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -10,3 +10,5 @@ python-dotenv==1.2.2 SQLAlchemy==2.0.49 typing_extensions==4.15.0 Werkzeug==3.1.8 +marshmallow==3.21.2 +APScheduler==3.10.4 \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html index f94d687..20a24d5 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,13 +1,45 @@ - - + + - - - - frontend + + + NDMS - Quản lý thiết bị mạng + + + + - -
- + + + + + + +
+
Đang tải...
+
+ + + - + \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5d057c4..6b9f948 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12,7 +12,8 @@ "axios": "^1.16.0", "react": "^19.2.5", "react-dom": "^19.2.5", - "react-router-dom": "^7.14.2" + "react-router-dom": "^7.14.2", + "tailwindcss": "^4.3.0" }, "devDependencies": { "@eslint/js": "^9.39.4", @@ -23,7 +24,8 @@ "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.2", "globals": "^17.5.0", - "vite": "^8.0.9" + "vite": "^8.0.12", + "vite-plugin-html": "^3.2.2" } }, "node_modules/@babel/code-frame": { @@ -267,9 +269,9 @@ } }, "node_modules/@emnapi/core": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", - "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", "dev": true, "license": "MIT", "optional": true, @@ -279,9 +281,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", - "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", "dev": true, "license": "MIT", "optional": true, @@ -555,6 +557,17 @@ "node": ">=6.0.0" } }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", @@ -592,10 +605,48 @@ "@emnapi/runtime": "^1.7.1" } }, + "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/@oxc-project/types": { - "version": "0.126.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.126.0.tgz", - "integrity": "sha512-oGfVtjAgwQVVpfBrbtk4e1XDyWHRFta6BS3GWVzrF8xYBT2VGQAk39yJS/wFSMrZqoiCU4oghT3Ch0HaHGIHcQ==", + "version": "0.129.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.129.0.tgz", + "integrity": "sha512-3oz8m3FGdr2nDXVqmFUw7jolKliC4MoyXYIG2c7gpjBnzUWQpUGIYcXYKxTdTi+N2jusvt610ckTMkxdwHkYEg==", "dev": true, "license": "MIT", "funding": { @@ -603,9 +654,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.16.tgz", - "integrity": "sha512-rhY3k7Bsae9qQfOtph2Pm2jZEA+s8Gmjoz4hhmx70K9iMQ/ddeae+xhRQcM5IuVx5ry1+bGfkvMn7D6MJggVSA==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0.tgz", + "integrity": "sha512-TWMZnRLMe63C2Lhyicviu7ZHaU4kxa6PS3rofvc9GmcvptzNN11BcfQ4Sl7MwTOsisQoa2keB/EBdNCAnUo8vA==", "cpu": [ "arm64" ], @@ -620,9 +671,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.16.tgz", - "integrity": "sha512-rNz0yK078yrNn3DrdgN+PKiMOW8HfQ92jQiXxwX8yW899ayV00MLVdaCNeVBhG/TbH3ouYVObo8/yrkiectkcQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0.tgz", + "integrity": "sha512-6XcD+8k0gPVItNagEw78/qqcBDwKcwDYS8V2hRmVsfUSIrd8cWe/CBvRDI5toqFyPfj+FJr6t8U6Xj2P2prEew==", "cpu": [ "arm64" ], @@ -637,9 +688,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.16.tgz", - "integrity": "sha512-r/OmdR00HmD4i79Z//xO06uEPOq5hRXdhw7nzkxQxwSavs3PSHa1ijntdpOiZ2mzOQ3fVVu8C1M19FoNM+dMUQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0.tgz", + "integrity": "sha512-iN/tWVXRQDWvmZlKdceP1Dwug9GDpEymhb9p4xnEe6zvCg5lFmzVljl+1qR1NVx3yfGpr2Na+CuLmv5IU8uzfQ==", "cpu": [ "x64" ], @@ -654,9 +705,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.16.tgz", - "integrity": "sha512-KcRE5w8h0OnjUatG8pldyD14/CQ5Phs1oxfR+3pKDjboHRo9+MkqQaiIZlZRpsxC15paeXme/I127tUa9TXJ6g==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0.tgz", + "integrity": "sha512-jjQMDvvwSOuhOwMszD/klSOjyWMM3zI64hWTj9KT5x4MxRbZAf+7vLQ6qouRhtsLVFHr3f0ILaJAfgENPiQdAQ==", "cpu": [ "x64" ], @@ -671,9 +722,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.16.tgz", - "integrity": "sha512-bT0guA1bpxEJ/ZhTRniQf7rNF8ybvXOuWbNIeLABaV5NGjx4EtOWBTSRGWFU9ZWVkPOZ+HNFP8RMcBokBiZ0Kg==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0.tgz", + "integrity": "sha512-d//Dtg2x6/m3mbV64yUGNnDGNZaDGRpDLLNGerHQUVObuNaIQaaDp25yUiqGXtHEXX+NP2d0wAlmKgpYgIAJ2A==", "cpu": [ "arm" ], @@ -688,9 +739,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.16.tgz", - "integrity": "sha512-+tHktCHWV8BDQSjemUqm/Jl/TPk3QObCTIjmdDy/nlupcujZghmKK2962LYrqFpWu+ai01AN/REOH3NEpqvYQg==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0.tgz", + "integrity": "sha512-n7Ofp0mx+aB2cC+Sdy5YtMnXtY9lchnHbY+3Yt0uq9JsWQExf4f5Whu0tK0R8Jdc9S6RchTHjIFY7uc92puOVQ==", "cpu": [ "arm64" ], @@ -705,9 +756,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.16.tgz", - "integrity": "sha512-3fPzdREH806oRLxpTWW1Gt4tQHs0TitZFOECB2xzCFLPKnSOy90gwA7P29cksYilFO6XVRY1kzga0cL2nRjKPg==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0.tgz", + "integrity": "sha512-EIVjy2cgd7uuMMo94FVkBp7F6DhcZAUwNURkSG3RwUmvAXR6s0ISxM81U+IydcZByPG0pZIHsf1b6kTxoFDgJA==", "cpu": [ "arm64" ], @@ -722,9 +773,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.16.tgz", - "integrity": "sha512-EKwI1tSrLs7YVw+JPJT/G2dJQ1jl9qlTTTEG0V2Ok/RdOenRfBw2PQdLPyjhIu58ocdBfP7vIRN/pvMsPxs/AQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0.tgz", + "integrity": "sha512-JEwwOPcwTLAcpDQlqSmjEmfs63xJnSiUNIGvLcDLUHCWK4XowpS/7c7tUsUH6uT/ct6bMUTdXKfI8967FYj6mg==", "cpu": [ "ppc64" ], @@ -739,9 +790,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.16.tgz", - "integrity": "sha512-Uknladnb3Sxqu6SEcqBldQyJUpk8NleooZEc0MbRBJ4inEhRYWZX0NJu12vNf2mqAq7gsofAxHrGghiUYjhaLQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0.tgz", + "integrity": "sha512-0wjCFhLrihtAubnT9iA0N++0pSV0z5Hg7tNGdNJ4RFaINceHadoF+kiFGyY1qSSNVIAZtLotG8Ju1bgDPkjnFA==", "cpu": [ "s390x" ], @@ -756,9 +807,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.16.tgz", - "integrity": "sha512-FIb8+uG49sZBtLTn+zt1AJ20TqVcqWeSIyoVt0or7uAWesgKaHbiBh6OpA/k9v0LTt+PTrb1Lao133kP4uVxkg==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0.tgz", + "integrity": "sha512-Dfn7iak9BcMMePxcoJfpSbWqnEyrp/dRF63/8qW/eHBdOZov6x5aShLLEYGYdIeSJ6vMLK/XCVB+lGIxm41bQA==", "cpu": [ "x64" ], @@ -773,9 +824,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.16.tgz", - "integrity": "sha512-RuERhF9/EgWxZEXYWCOaViUWHIboceK4/ivdtQ3R0T44NjLkIIlGIAVAuCddFxsZ7vnRHtNQUrt2vR2n2slB2w==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0.tgz", + "integrity": "sha512-5/utzzDmD/pD/bmuaUcbTf/sZYy0aztwIVlfpoW1fTjCZ0BaPOMVWGZL1zvgxyi7ZIVYWlxKONHmSbHuiOh8Jw==", "cpu": [ "x64" ], @@ -790,9 +841,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.16.tgz", - "integrity": "sha512-mXcXnvd9GpazCxeUCCnZ2+YF7nut+ZOEbE4GtaiPtyY6AkhZWbK70y1KK3j+RDhjVq5+U8FySkKRb/+w0EeUwA==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0.tgz", + "integrity": "sha512-ouJs8VcUomfLfpbUECqFMRqdV4x6aeAK3MA4m6vTrJJjKyWTV5KnxZx7Jd9G+GlDaQQxubcba00x16OyJ1meig==", "cpu": [ "arm64" ], @@ -807,9 +858,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.16.tgz", - "integrity": "sha512-3Q2KQxnC8IJOLqXmUMoYwyIPZU9hzRbnHaoV3Euz+VVnjZKcY8ktnNP8T9R4/GGQtb27C/UYKABxesKWb8lsvQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0.tgz", + "integrity": "sha512-E+oHKGiDA+lsKMmFtffDDw91EryDT7uJocrIuCHqhm6bCTM6xFK+3gaCkYOHfPwQr0cCNarSM2xaELoQDz9jJg==", "cpu": [ "wasm32" ], @@ -817,8 +868,8 @@ "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "1.9.2", - "@emnapi/runtime": "1.9.2", + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "engines": { @@ -826,9 +877,9 @@ } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.16.tgz", - "integrity": "sha512-tj7XRemQcOcFwv7qhpUxMTBbI5mWMlE4c1Omhg5+h8GuLXzyj8HviYgR+bB2DMDgRqUE+jiDleqSCRjx4aYk/Q==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0.tgz", + "integrity": "sha512-yYK02n8Rngo+gbm1y6G0+7jk1sJ/2Wt7K0me0Y7k/ErBpyf+LJ2gFpqWVTcRV1rUepBlQRmpgWkTQCiiwrK0Ow==", "cpu": [ "arm64" ], @@ -843,9 +894,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.16.tgz", - "integrity": "sha512-PH5DRZT+F4f2PTXRXR8uJxnBq2po/xFtddyabTJVJs/ZYVHqXPEgNIr35IHTEa6bpa0Q8Awg+ymkTaGnKITw4g==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0.tgz", + "integrity": "sha512-14bpChMahXRRXiTwahSl+zzHPW6qQTXtkMuJBFlbo+pqSAews2d4BdCSHfrJ/MBsCZtpmTafsY+1QhBzitcmdg==", "cpu": [ "x64" ], @@ -866,6 +917,33 @@ "dev": true, "license": "MIT" }, + "node_modules/@rollup/pluginutils": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", + "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "estree-walker": "^2.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/@rollup/pluginutils/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/@tanstack/query-core": { "version": "5.100.9", "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.9.tgz", @@ -893,9 +971,9 @@ } }, "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", "dev": true, "license": "MIT", "optional": true, @@ -1026,6 +1104,13 @@ "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==", + "dev": true, + "license": "MIT" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -1063,6 +1148,13 @@ "node": ">=6.0.0" } }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, "node_modules/brace-expansion": { "version": "1.1.14", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", @@ -1074,6 +1166,19 @@ "concat-map": "0.0.1" } }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/browserslist": { "version": "4.28.2", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", @@ -1108,6 +1213,13 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -1131,6 +1243,17 @@ "node": ">=6" } }, + "node_modules/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001788", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz", @@ -1169,6 +1292,19 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/clean-css": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", + "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "source-map": "~0.6.0" + }, + "engines": { + "node": ">= 10.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1189,6 +1325,13 @@ "dev": true, "license": "MIT" }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "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", @@ -1201,6 +1344,16 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1208,6 +1361,23 @@ "dev": true, "license": "MIT" }, + "node_modules/connect-history-api-fallback": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", + "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/consola": { + "version": "2.15.3", + "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", + "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==", + "dev": true, + "license": "MIT" + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -1243,6 +1413,36 @@ "node": ">= 8" } }, + "node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -1294,6 +1494,99 @@ "node": ">=8" } }, + "node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-8.0.3.tgz", + "integrity": "sha512-SErOMvge0ZUyWd5B0NXMQlDkN+8r+HhVUsxgOO7IoPDOdDRD2JjExpN6y3KnFR66jsJMwSn1pqIivhU5rcJiNg==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -1308,6 +1601,22 @@ "node": ">= 0.4" } }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.343", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.343.tgz", @@ -1315,6 +1624,16 @@ "dev": true, "license": "ISC" }, + "node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -1557,6 +1876,13 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -1574,6 +1900,36 @@ "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", @@ -1588,6 +1944,16 @@ "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", @@ -1619,6 +1985,52 @@ "node": ">=16.0.0" } }, + "node_modules/filelist": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", + "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "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", @@ -1693,6 +2105,21 @@ "node": ">= 6" } }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1802,6 +2229,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -1851,6 +2285,16 @@ "node": ">= 0.4" } }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, "node_modules/hermes-estree": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", @@ -1868,6 +2312,28 @@ "hermes-estree": "0.25.1" } }, + "node_modules/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "camel-case": "^4.1.2", + "clean-css": "^5.2.2", + "commander": "^8.3.0", + "he": "^1.2.0", + "param-case": "^3.0.4", + "relateurl": "^0.2.7", + "terser": "^5.10.0" + }, + "bin": { + "html-minifier-terser": "cli.js" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -1928,6 +2394,16 @@ "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/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -1935,6 +2411,24 @@ "dev": true, "license": "ISC" }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -2002,6 +2496,19 @@ "node": ">=6" } }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -2310,6 +2817,16 @@ "dev": true, "license": "MIT" }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -2329,6 +2846,43 @@ "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.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -2371,9 +2925,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", "dev": true, "funding": [ { @@ -2396,6 +2950,28 @@ "dev": true, "license": "MIT" }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/node-html-parser": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-5.4.2.tgz", + "integrity": "sha512-RaBPP3+51hPne/OolXxcz89iYvQvKOydaqoePpOgXcrOKZhjVIzmpKZz+Hd/RBO2/zN2q6CNJhQzucVz+u3Jyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-select": "^4.2.1", + "he": "1.2.0" + } + }, "node_modules/node-releases": { "version": "2.0.38", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", @@ -2403,6 +2979,19 @@ "dev": true, "license": "MIT" }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -2453,6 +3042,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -2466,6 +3066,17 @@ "node": ">=6" } }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -2486,6 +3097,13 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-0.2.0.tgz", + "integrity": "sha512-sTitTPYnn23esFR3RlqYBWn4c45WGeLcsKzQiUpXJAyfcWkolvlYpV8FLo7JishK946oQwMFUCHXQ9AjGPKExw==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -2507,9 +3125,9 @@ } }, "node_modules/postcss": { - "version": "8.5.10", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", - "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", "dev": true, "funding": [ { @@ -2564,6 +3182,27 @@ "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.5", "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", @@ -2623,6 +3262,16 @@ "react-dom": ">=18" } }, + "node_modules/relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -2633,15 +3282,26 @@ "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/rolldown": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.16.tgz", - "integrity": "sha512-rzi5WqKzEZw3SooTt7cgm4eqIoujPIyGcJNGFL7iPEuajQw7vxMHUkXylu4/vhCkJGXsgRmxqMKXUpT6FEgl0g==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0.tgz", + "integrity": "sha512-yD986aXDESFGS95spT1LAv0jssywP4npMEjmMHyN2/5+eE8qQJUype2AaKkRiLgBgyD0LFlubwAht7VmY8rGoA==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.126.0", - "@rolldown/pluginutils": "1.0.0-rc.16" + "@oxc-project/types": "=0.129.0", + "@rolldown/pluginutils": "1.0.0" }, "bin": { "rolldown": "bin/cli.mjs" @@ -2650,30 +3310,54 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-rc.16", - "@rolldown/binding-darwin-arm64": "1.0.0-rc.16", - "@rolldown/binding-darwin-x64": "1.0.0-rc.16", - "@rolldown/binding-freebsd-x64": "1.0.0-rc.16", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.16", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.16", - "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.16", - "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.16", - "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.16", - "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.16", - "@rolldown/binding-linux-x64-musl": "1.0.0-rc.16", - "@rolldown/binding-openharmony-arm64": "1.0.0-rc.16", - "@rolldown/binding-wasm32-wasi": "1.0.0-rc.16", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.16", - "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.16" + "@rolldown/binding-android-arm64": "1.0.0", + "@rolldown/binding-darwin-arm64": "1.0.0", + "@rolldown/binding-darwin-x64": "1.0.0", + "@rolldown/binding-freebsd-x64": "1.0.0", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0", + "@rolldown/binding-linux-arm64-gnu": "1.0.0", + "@rolldown/binding-linux-arm64-musl": "1.0.0", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0", + "@rolldown/binding-linux-s390x-gnu": "1.0.0", + "@rolldown/binding-linux-x64-gnu": "1.0.0", + "@rolldown/binding-linux-x64-musl": "1.0.0", + "@rolldown/binding-openharmony-arm64": "1.0.0", + "@rolldown/binding-wasm32-wasi": "1.0.0", + "@rolldown/binding-win32-arm64-msvc": "1.0.0", + "@rolldown/binding-win32-x64-msvc": "1.0.0" } }, "node_modules/rolldown/node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.16.tgz", - "integrity": "sha512-45+YtqxLYKDWQouLKCrpIZhke+nXxhsw+qAHVzHDVwttyBlHNBVs2K25rDXrZzhpTp9w1FlAlvweV1H++fdZoA==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0.tgz", + "integrity": "sha512-aKs/3GSWyV0mrhNmt/96/Z3yczC3yvrzYATCiCXQebBsGyYzjNdUphRVLeJQ67ySKVXRfMxt2lm12pmXvbPFQQ==", "dev": true, "license": "MIT" }, + "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/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -2719,6 +3403,16 @@ "node": ">=8" } }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -2729,6 +3423,17 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -2755,6 +3460,38 @@ "node": ">=8" } }, + "node_modules/tailwindcss": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz", + "integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==", + "license": "MIT" + }, + "node_modules/terser": { + "version": "5.47.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.47.1.tgz", + "integrity": "sha512-tPbLXTI6ohPASb/1YViL428oEHu6/qv1OxqYnfaonVCFHqx4+wCd95pHrQWsL5X4pl90CTyW9piSAsS2L0VoMw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.16", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", @@ -2772,13 +3509,25 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "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/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, - "license": "0BSD", - "optional": true + "license": "0BSD" }, "node_modules/type-check": { "version": "0.4.0", @@ -2793,6 +3542,16 @@ "node": ">= 0.8.0" } }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.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", @@ -2835,16 +3594,16 @@ } }, "node_modules/vite": { - "version": "8.0.9", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.9.tgz", - "integrity": "sha512-t7g7GVRpMXjNpa67HaVWI/8BWtdVIQPCL2WoozXXA7LBGEFK4AkkKkHx2hAQf5x1GZSlcmEDPkVLSGahxnEEZw==", + "version": "8.0.12", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.12.tgz", + "integrity": "sha512-w2dDofOWv2QB09ZITZBsvKTVAlYvPR4IAmrY/v0ir9KvLs0xybR7i48wxhM1/oyBWO34wPns+bPGw5ZrZqDpZg==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", - "postcss": "^8.5.10", - "rolldown": "1.0.0-rc.16", + "postcss": "^8.5.14", + "rolldown": "1.0.0", "tinyglobby": "^0.2.16" }, "bin": { @@ -2861,7 +3620,7 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.1.0", + "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", @@ -2912,6 +3671,30 @@ } } }, + "node_modules/vite-plugin-html": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/vite-plugin-html/-/vite-plugin-html-3.2.2.tgz", + "integrity": "sha512-vb9C9kcdzcIo/Oc3CLZVS03dL5pDlOFuhGlZYDCJ840BhWl/0nGeZWf3Qy7NlOayscY4Cm/QRgULCQkEZige5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^4.2.0", + "colorette": "^2.0.16", + "connect-history-api-fallback": "^1.6.0", + "consola": "^2.15.3", + "dotenv": "^16.0.0", + "dotenv-expand": "^8.0.2", + "ejs": "^3.1.6", + "fast-glob": "^3.2.11", + "fs-extra": "^10.0.1", + "html-minifier-terser": "^6.1.0", + "node-html-parser": "^5.3.3", + "pathe": "^0.2.0" + }, + "peerDependencies": { + "vite": ">=2.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 502f9fb..3776f89 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,7 +14,8 @@ "axios": "^1.16.0", "react": "^19.2.5", "react-dom": "^19.2.5", - "react-router-dom": "^7.14.2" + "react-router-dom": "^7.14.2", + "tailwindcss": "^4.3.0" }, "devDependencies": { "@eslint/js": "^9.39.4", @@ -25,6 +26,7 @@ "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.2", "globals": "^17.5.0", - "vite": "^8.0.9" + "vite": "^8.0.12", + "vite-plugin-html": "^3.2.2" } } diff --git a/frontend/src/features/dashboard/dashboard.js b/frontend/src/features/dashboard/dashboard.js new file mode 100644 index 0000000..2dfdbb5 --- /dev/null +++ b/frontend/src/features/dashboard/dashboard.js @@ -0,0 +1,114 @@ +// ============================================ +// FILE NÀY LÀ TRANG CHỦ (DASHBOARD) +// Nhiệm vụ: Tạo ra giao diện và dữ liệu cho trang chủ +// ============================================ + +// Hàm này được gọi từ main.js +export async function showDashboard(container) { + // 1. TẠO GIAO DIỆN HTML + container.innerHTML = ` +
+ +
+

Chào mừng đến với NDMS

+

Hệ thống quản lý thiết bị mạng tập trung

+
+ + +
+ +
+
+
+

Tổng thiết bị

+

0

+
+ +
+
+ + +
+
+
+

Đang hoạt động

+

0

+
+ +
+
+ + +
+
+
+

Đang tắt

+

0

+
+ +
+
+ + +
+
+
+

Cảnh báo

+

0

+
+ +
+
+
+ + +
+

+ Hoạt động gần đây +

+
+ +
+
+
+ `; + + // 2. ĐỔ DỮ LIỆU VÀO CÁC Ô THỐNG KÊ + // (Đây là dữ liệu mẫu - sau này sẽ gọi API từ backend) + document.getElementById('stat-total').innerText = '24'; + document.getElementById('stat-up').innerText = '18'; + document.getElementById('stat-down').innerText = '6'; + document.getElementById('stat-alert').innerText = '3'; + + // 3. ĐỔ DỮ LIỆU VÀO DANH SÁCH HOẠT ĐỘNG + const activities = [ + { time: '10:30', action: 'Switch Tầng 1', status: 'down', message: 'Mất kết nối' }, + { time: '09:15', action: 'Router Chính', status: 'up', message: 'Đã kết nối lại' }, + { time: '08:00', action: 'Server DB', status: 'warning', message: 'CPU quá tải' }, + { time: 'Hôm qua', action: 'Cập nhật hệ thống', status: 'info', message: 'Đã cập nhật cấu hình' }, + ]; + + // Tạo HTML cho từng hoạt động + let activityHtml = ''; + for (let act of activities) { + let iconClass = ''; + if (act.status === 'down') iconClass = 'text-red-500 fa-exclamation-circle'; + else if (act.status === 'up') iconClass = 'text-green-500 fa-check-circle'; + else if (act.status === 'warning') iconClass = 'text-yellow-500 fa-exclamation-triangle'; + else iconClass = 'text-blue-500 fa-info-circle'; + + activityHtml += ` +
+ +
+
+ ${act.action} + ${act.time} +
+

${act.message}

+
+
+ `; + } + document.getElementById('activity-list').innerHTML = activityHtml; +} \ No newline at end of file diff --git a/frontend/src/features/device-type/device-type-api.js b/frontend/src/features/device-type/device-type-api.js new file mode 100644 index 0000000..fa5c63b --- /dev/null +++ b/frontend/src/features/device-type/device-type-api.js @@ -0,0 +1,52 @@ +// ============================================ +// API: KẾT NỐI VỚI BACK-END +// ============================================ + +// Địa chỉ Back-end (chạy cổng 5000) +const API_BASE_URL = 'http://localhost:5000/api'; + +// Hàm gọi API chung +async function callAPI(endpoint, method = 'GET', body = null) { + const options = { + method: method, + headers: { + 'Content-Type': 'application/json', + } + }; + + if (body && (method === 'POST' || method === 'PUT')) { + options.body = JSON.stringify(body); + } + + try { + const response = await fetch(`${API_BASE_URL}${endpoint}`, options); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.message || 'Có lỗi xảy ra'); + } + + return await response.json(); + } catch (error) { + console.error('API Error:', error); + throw error; + } +} + +// Các hàm API cho Device Type +export const deviceTypeAPI = { + // Lấy danh sách tất cả loại thiết bị + getAll: () => callAPI('/device-types'), + + // Lấy chi tiết 1 loại thiết bị + getById: (id) => callAPI(`/device-types/${id}`), + + // Thêm mới loại thiết bị + create: (data) => callAPI('/device-types', 'POST', data), + + // Cập nhật loại thiết bị + update: (id, data) => callAPI(`/device-types/${id}`, 'PUT', data), + + // Xóa loại thiết bị + delete: (id) => callAPI(`/device-types/${id}`, 'DELETE'), +}; \ No newline at end of file diff --git a/frontend/src/features/device-type/device-type.js b/frontend/src/features/device-type/device-type.js new file mode 100644 index 0000000..76f663a --- /dev/null +++ b/frontend/src/features/device-type/device-type.js @@ -0,0 +1,474 @@ +// // ============================================ +// // MODULE: QUẢN LÝ LOẠI THIẾT BỊ (DEVICE TYPE) +// // ============================================ +// // Nhiệm vụ: Thêm, sửa, xóa, hiển thị danh sách loại thiết bị +// // ============================================ + +// // DỮ LIỆU MẪU (lưu trong bộ nhớ tạm) +// let deviceTypes = [ +// { id: '1', name: 'Switch', description: 'Thiết bị chuyển mạch mạng', color: '#10B981', isActive: true }, +// { id: '2', name: 'Router', description: 'Bộ định tuyến mạng', color: '#3B82F6', isActive: true }, +// { id: '3', name: 'WorkStation', description: 'Máy trạm làm việc', color: '#8B5CF6', isActive: true }, +// { id: '4', name: 'Server', description: 'Máy chủ', color: '#EF4444', isActive: true }, +// ]; + +// // HÀM HIỂN THỊ TRANG DEVICE TYPE +// export async function showDeviceTypePage(container) { +// // Tạo giao diện HTML +// container.innerHTML = ` +//
+// +//
+//
+//

+// Quản lý loại thiết bị +//

+//

Danh mục các loại thiết bị mạng trong hệ thống

+//
+// +//
+ +// +//
+//
+//
+//
+//

Tổng số loại

+//

0

+//
+// +//
+//
+//
+//
+//
+//

Đang hoạt động

+//

0

+//
+// +//
+//
+//
+ +// +//
+// +// +//
+ +// +//
+// +//
Đang tải...
+//
+//
+// `; + +// // HIỂN THỊ DANH SÁCH +// renderList(); + +// // CẬP NHẬT THỐNG KÊ +// updateStats(); + +// // GẮN SỰ KIỆN CHO NÚT THÊM +// document.getElementById('addDeviceTypeBtn').onclick = showAddForm; + +// // GẮN SỰ KIỆN CHO Ô TÌM KIẾM +// document.getElementById('searchInput').oninput = function(e) { +// const keyword = e.target.value.toLowerCase(); +// const filtered = deviceTypes.filter(item => +// item.name.toLowerCase().includes(keyword) || +// (item.description && item.description.toLowerCase().includes(keyword)) +// ); +// renderListWithData(filtered); +// }; +// } + +// // HÀM HIỂN THỊ DANH SÁCH (lấy từ mảng deviceTypes) +// function renderList() { +// renderListWithData(deviceTypes); +// } + +// // HÀM HIỂN THỊ DANH SÁCH VỚI DỮ LIỆU TÙY CHỈNH +// function renderListWithData(data) { +// const container = document.getElementById('deviceTypeList'); + +// if (!container) return; + +// if (data.length === 0) { +// container.innerHTML = ` +//
+// +//

Chưa có loại thiết bị nào

+// +//
+// `; +// return; +// } + +// let html = ''; +// for (let item of data) { +// html += ` +//
+//
+//
+// +//
+// +//
+// +//
+//
+//

${escapeHtml(item.name)}

+// +// ${item.isActive ? '● Hoạt động' : '○ Dừng'} +// +//
+//

${escapeHtml(item.description || 'Chưa có mô tả')}

+//
+// ID: ${item.id} +// Mã màu: ${item.color} +//
+//
+//
+// +//
+// +// +//
+//
+//
+// `; +// } +// container.innerHTML = html; +// } + +// // CẬP NHẬT SỐ LIỆU THỐNG KÊ +// function updateStats() { +// const totalEl = document.getElementById('totalCount'); +// const activeEl = document.getElementById('activeCount'); + +// if (totalEl) totalEl.innerText = deviceTypes.length; +// if (activeEl) activeEl.innerText = deviceTypes.filter(item => item.isActive).length; +// } + +// // HIỂN THỊ FORM THÊM MỚI +// function showAddForm() { +// // Tạo modal đơn giản bằng prompt (dễ hiểu cho người mới) +// const name = prompt('📝 Nhập tên loại thiết bị:', ''); +// if (!name) return; + +// const description = prompt('📄 Nhập mô tả (không bắt buộc):', ''); +// const color = prompt('🎨 Nhập mã màu (ví dụ: #3B82F6, #10B981, #EF4444):', '#3B82F6'); + +// // Tạo mới +// const newType = { +// id: Date.now().toString(), +// name: name, +// description: description || '', +// color: color || '#3B82F6', +// isActive: true +// }; + +// deviceTypes.push(newType); + +// // Cập nhật lại giao diện +// renderList(); +// updateStats(); + +// alert(`✅ Đã thêm loại thiết bị "${name}" thành công!`); +// } + +// // HIỂN THỊ FORM SỬA +// window.editDeviceType = function(id) { +// const item = deviceTypes.find(t => t.id === id); +// if (!item) return; + +// const newName = prompt('✏️ Sửa tên loại thiết bị:', item.name); +// if (!newName) return; + +// const newDesc = prompt('📄 Sửa mô tả:', item.description || ''); +// const newColor = prompt('🎨 Sửa mã màu:', item.color); +// const isActive = confirm(`✅ Loại thiết bị này có đang hoạt động không?\nOK = Có, Cancel = Không`); + +// // Cập nhật +// item.name = newName; +// item.description = newDesc || ''; +// item.color = newColor || '#3B82F6'; +// item.isActive = isActive; + +// // Cập nhật lại giao diện +// renderList(); +// updateStats(); + +// alert(`✅ Đã cập nhật "${newName}" thành công!`); +// }; + +// // XÓA LOẠI THIẾT BỊ +// window.deleteDeviceType = function(id) { +// const item = deviceTypes.find(t => t.id === id); +// if (!item) return; + +// if (confirm(`⚠️ Bạn có chắc muốn xóa "${item.name}"?\nHành động này không thể hoàn tác!`)) { +// deviceTypes = deviceTypes.filter(t => t.id !== id); + +// // Cập nhật lại giao diện +// renderList(); +// updateStats(); + +// alert(`🗑️ Đã xóa "${item.name}" thành công!`); +// } +// }; + +// // Hàm bảo vệ chống mã độc XSS +// function escapeHtml(str) { +// if (!str) return ''; +// return str +// .replace(/&/g, '&') +// .replace(//g, '>') +// .replace(/"/g, '"') +// .replace(/'/g, '''); +// } + +// // Export để dùng ở nơi khác (nếu cần) +// export { deviceTypes, renderList, updateStats }; + + +// ============================================ +// UI: GIAO DIỆN QUẢN LÝ LOẠI THIẾT BỊ +// ============================================ + +import { deviceTypeAPI } from './device-type-api.js'; + +let deviceTypes = []; + +// HÀM HIỂN THỊ TRANG DEVICE TYPE +export async function showDeviceTypePage(container) { + // Tạo giao diện HTML + container.innerHTML = ` +
+ +
+
+

+ Quản lý loại thiết bị +

+

Danh mục các loại thiết bị mạng

+
+ +
+ + +
+
+

Tổng số loại

+

0

+
+
+

Đang hoạt động

+

0

+
+
+ + +
+ + +
+ + +
+
Đang tải dữ liệu...
+
+
+ `; + + // Load dữ liệu từ Back-end + await loadDataFromAPI(); + + // Gắn sự kiện + document.getElementById('addDeviceTypeBtn').onclick = showAddForm; + document.getElementById('searchInput').oninput = handleSearch; +} + +// LOAD DỮ LIỆU TỪ BACK-END +async function loadDataFromAPI() { + try { + // Gọi API lấy danh sách + const result = await deviceTypeAPI.getAll(); + deviceTypes = Array.isArray(result) ? result : result.data || []; + + // Hiển thị lên giao diện + renderList(deviceTypes); + updateStats(); + + console.log('Đã tải', deviceTypes.length, 'loại thiết bị'); + } catch (error) { + console.error('Lỗi tải dữ liệu:', error); + document.getElementById('deviceTypeList').innerHTML = ` +
+ +

Không thể kết nối đến Back-end!

+

Vui lòng đảm bảo Back-end đang chạy tại http://localhost:5000

+
+ `; + } +} + +// HIỂN THỊ DANH SÁCH +function renderList(data) { + const container = document.getElementById('deviceTypeList'); + + if (!container) return; + + if (data.length === 0) { + container.innerHTML = ` +
+ +

Chưa có loại thiết bị nào

+
+ `; + return; + } + + let html = ''; + for (let item of data) { + html += ` +
+
+
+
+ +
+
+

${escapeHtml(item.name)}

+

${escapeHtml(item.description || 'Chưa có mô tả')}

+ + ${item.isActive ? '● Hoạt động' : '○ Dừng'} + +
+
+
+ + +
+
+
+ `; + } + container.innerHTML = html; +} + +// CẬP NHẬT THỐNG KÊ +function updateStats() { + const totalEl = document.getElementById('totalCount'); + const activeEl = document.getElementById('activeCount'); + + if (totalEl) totalEl.innerText = deviceTypes.length; + if (activeEl) activeEl.innerText = deviceTypes.filter(item => item.isActive).length; +} + +// TÌM KIẾM +function handleSearch(e) { + const keyword = e.target.value.toLowerCase(); + const filtered = deviceTypes.filter(item => + item.name.toLowerCase().includes(keyword) || + (item.description && item.description.toLowerCase().includes(keyword)) + ); + renderList(filtered); +} + +// THÊM MỚI (GỌI API) +async function showAddForm() { + const name = prompt('📝 Nhập tên loại thiết bị:'); + if (!name) return; + + const description = prompt('📄 Nhập mô tả:'); + const color = prompt('🎨 Nhập mã màu (vd: #3B82F6):', '#3B82F6'); + + try { + const newType = await deviceTypeAPI.create({ + name: name, + description: description || '', + color: color || '#3B82F6', + isActive: true + }); + + // Tải lại dữ liệu + await loadDataFromAPI(); + alert(`✅ Đã thêm "${name}" thành công!`); + } catch (error) { + alert('❌ Lỗi: ' + error.message); + } +} + +// SỬA (GỌI API) +window.editDeviceType = async function(id) { + const item = deviceTypes.find(t => t.id === id); + if (!item) return; + + const newName = prompt('✏️ Sửa tên:', item.name); + if (!newName) return; + + const newDesc = prompt('📄 Sửa mô tả:', item.description || ''); + const newColor = prompt('🎨 Sửa màu:', item.color); + const isActive = confirm('Có đang hoạt động không?'); + + try { + await deviceTypeAPI.update(id, { + ...item, + name: newName, + description: newDesc || '', + color: newColor || '#3B82F6', + isActive: isActive + }); + + await loadDataFromAPI(); + alert('✅ Cập nhật thành công!'); + } catch (error) { + alert('❌ Lỗi: ' + error.message); + } +}; + +// XÓA (GỌI API) +window.deleteDeviceType = async function(id) { + const item = deviceTypes.find(t => t.id === id); + if (!item) return; + + if (confirm(`⚠️ Xóa "${item.name}"?`)) { + try { + await deviceTypeAPI.delete(id); + await loadDataFromAPI(); + alert('🗑️ Xóa thành công!'); + } catch (error) { + alert('❌ Lỗi: ' + error.message); + } + } +}; + +function escapeHtml(str) { + if (!str) return ''; + return str.replace(/[&<>]/g, function(m) { + if (m === '&') return '&'; + if (m === '<') return '<'; + if (m === '>') return '>'; + return m; + }); +} \ No newline at end of file diff --git a/frontend/src/main.js b/frontend/src/main.js new file mode 100644 index 0000000..3810f5f --- /dev/null +++ b/frontend/src/main.js @@ -0,0 +1,57 @@ +// ============================================ +// FILE NÀY CÓ NHIỆM VỤ: ĐIỀU KHIỂN CHUYỂN TRANG +// ============================================ + +// Import các module (tính năng) của ứng dụng +import { showDashboard } from './features/dashboard/dashboard.js'; +import { showDeviceTypePage } from './features/device-type/device-type.js'; + +// Hàm chuyển đổi giữa các trang +async function switchToPage(pageName) { + // Lấy thẻ div chứa nội dung + const appDiv = document.getElementById('app'); + + // Debug: In ra console để kiểm tra xem hàm có được gọi đúng không + if (!appDiv) { + console.error('Không tìm thấy element #app'); + return; + } + + console.log('Chuyển đến trang:', pageName); // Debug + + + // Kiểm tra xem người dùng bấm vào trang nào + if (pageName === 'dashboard') { + // Gọi hàm showDashboard từ file dashboard.js + await showDashboard(appDiv); + } + else if (pageName === 'device-type') { + await showDeviceTypePage(appDiv); + } + else if (pageName === 'device') { + appDiv.innerHTML = '
🖥️ Trang Thiết bị - Đang phát triển
'; + } + else if (pageName === 'alert') { + appDiv.innerHTML = '
🔔 Trang Cảnh báo - Đang phát triển
'; + } + + // Đổi màu nút menu đang được chọn + document.querySelectorAll('.menu-btn').forEach(btn => { + if (btn.dataset.page === pageName) { + btn.className = 'menu-btn bg-indigo-600 text-white px-5 py-2 rounded-lg font-medium'; + } else { + btn.className = 'menu-btn bg-gray-200 text-gray-700 px-5 py-2 rounded-lg font-medium'; + } + }); +} + +// BẮT SỰ KIỆN KHI BẤM VÀO CÁC NÚT MENU +document.querySelectorAll('.menu-btn').forEach(button => { + button.addEventListener('click', () => { + const pageName = button.dataset.page; // lấy tên trang từ thuộc tính data-page + switchToPage(pageName); + }); +}); + +// ⭐ QUAN TRỌNG: Khi load trang, mặc định hiển thị DASHBOARD (trang chủ) +switchToPage('dashboard'); \ No newline at end of file diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 8b0f57b..bd295b8 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -1,7 +1,22 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' +import { defineConfig } from 'vite'; +import { createHtmlPlugin } from 'vite-plugin-html'; -// https://vite.dev/config/ export default defineConfig({ - plugins: [react()], -}) + plugins: [ + createHtmlPlugin({ + minify: true, + entry: '/src/main.js', + template: 'index.html', + }), + ], + server: { + port: 5173, + open: true, + proxy: { + '/api': { + target: 'http://localhost:5000', // Backend Flask của bạn + changeOrigin: true, + } + } + } +}); \ No newline at end of file diff --git a/spec_output.txt b/spec_output.txt new file mode 100644 index 0000000..f440faf --- /dev/null +++ b/spec_output.txt @@ -0,0 +1,579 @@ + + +PHẦN MỀM QUẢN LÝ THIẾT BỊ MẠNG +Network Device Management System + +ĐẶC TẢ PHẦN MỀM +Software Specification Document + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Công Nghệ + + + • Tổng Quan Hệ Thống +Phần mềm Quản Lý Thiết Bị Mạng (Network Device Management System – NDMS) là hệ thống fullstack phụ việc kiểm tra, giám sát, quản lý tập trung các thiết bị mạng trong tổ chức. Hệ thống tích hợp bản đồ số để trực quan hóa vị trí và trạng thái thiết bị theo thời gian thực. + • Tính năng cốt lõi: + • Hiển thị thông tin và trạng thái thiết bị trự quan trên bản đồ + • Tìm kiếm, lọc thiết bị theo thông tin không gian (Vùng Bản Đồ) và thuộc tính (loại, trạng thái, IP) + • Giám sát trạng thái thiết bị thời gian thực qua Ping và SNMP (Hai phương thức) + • Lưu trữ và hiển thị lịch sử trạng thái + • Cảnh báo tự động khi thiết bị down qua giao diện web hoặc email + • Công Nghệ và Kiến Trúc Tổng Thể + +Postgresql +Database +Flask Python +Backend APIs +Reactjs +Frontend Web + + + + Kiến Trúc Tổng Thể: + + + + + + + + + + + + + + + + + + + + + + + + + + + • Quản Lý Danh mục loại thiết bị (DeviceType) + • Mô tả chức năng +Module Device Type cho phép người dùng quản lý danh mục các loại thiết bị mạng được hỗ trợ trong hệ thống. Mỗi loại thiết bị có tên, mô tả, icon đại diện và màu sắc hiển thị trên bản đồ. Dữ liệu danh mục này là nền tảng cho module Device – Mỗi thiết bị muốn quản lý phải nằm trong danh mục loại thiết bị này. + • Loại thiết bị hỗ trợ: Máy Trạm (WorkStation), Switch, Router, Máy Tính, Điện thoại, …. + • Mỗi loại có icon và màu sắc riêng – Dùng để hiển thị trên bản đồ + • Không được xóa loại thiết đang được gán cho thiết bị nào đó (FK) + • Mô tả quy trình +Luồng Quản lý danh mục thiết bị: + Truy cập trang quản lý -> Danh mục loại thiết bị ++ Thêm loại thiết bị mới: Nhập Tên, Mô Tả, Chọn icon (Upload SVG/ PNG), chọn màu sắc mặc định ++ Hệ thống validate: Tên không trùng lặp (UNIQUE), icon đúng định dạng + + • Giải Pháp & Công Nghệ ++ Lưu icon dưới dạng URL (Upload lên S3-compatible storage hoặc server static file)+ Api danh sách trả về không phân trang -> Hiển thị dưới dạng dropdown để người dùng có thể chọn + + • CSDL +DeviceType +Name +Type +Nullable +ID +UUID +False +Name +VARCHAR(100) +False +Description +TEXT +True +IconUrl +VARCHAR(512) +True +Color +VARCHAR(7) +False +SortOrder +SMALLINT +False +IsActive +BOOLEAN +False +Created +TIMESTAMPTZ +False +Modified +TIMESTAMPTZ +False + + + + + + + + • API +API +METHOD +ROUTES +Danh Sách +GET +/device-types +/device-types/{DeviceTypeId} +Thêm Mới +POST +/device-types +Cập Nhật +PUT +/device-types/{deviceTypeId} +Xóa +DELETE +/device-types/{deviceTypeId} + +*Không cho phép xóa khi còn thiết bị thuộc loại này + + + + + + + • Quản Lý Thiết Bị (Device) -> một device type có nhiều device khác nhau + • Mô Tả Chức Năng +Module Device là phần trung tâm của hệ thống – quản lý toàn bộ các thông tin các thiết bị mạng cần giám sát. Mỗi thiết bị có đầy đủ thông tin nhận dạng, vị trí địa lý (tọa độ cần hiển thị trên bản đồ, thông tin kết nối mạng(IP/Port), và giao thức giám sát được áp dụng. + • Mỗi thiết bị bắt buộc thuộc một DeviceType đã được định nghĩa + • Tọa độ Iat/Ing nhập bằng cách click trực tiếp trên bản đồ Leaflet min trong form thêm thiết bị + • Sau khi tạo, APScheduler tự động thêm job Ping/SNMP cho thiết bị theo MonitorConfig + • Trạng thái hiện tại (CurrentStatus) được tính từ bản ghi mới nhất trong bảng DeviceStatus + • Hỗ trợ upload hình ảnh đại diện riêng cho từng thiết bị + + + • Mô Tả Quy Trình +Luồng thêm mới thiết bị: ++ Người dùng truy cập danh sách -> Thêm Mới thiết bị ++ Điền thông tin thiết bị: Device Type, Mô Tả, Ip Address, Màu Sắc, Avatar. ++ Validate: IP hợp lệ (regex IPv4/IPv6), tên không trùng, Iat, Ing ++ Lưu vào bảng devices ++ APSscheduler.add_job() tự động được gọi – Thêm job Ping hoặc SNMP theo MonitorConfig mặc định ++ Thiết bị xuất hiện ngay trên Leafet map với trạng thái Unknow ++ Ping worker chạy lần đầu theo interval cấu hình -> Cập nhật trạng thái Up hoặc Down + + + • Giải Pháp & Công Nghệ + + • Tọa độ lưu FLOAT8 + • Khi thiết bị di rời vật lí: Bấm vào sửa Iat/Ing trên form sửa -> Marker tự dịch chuyển trên map + • Avatar upload : Upload qua S3 cloud để lưu trữ + + + • CSDL +Device Type +Name +Type +Nullable +ID +UUID +False +DeviceTypeId +UUID +False +Name +VARCHAR(200) +False +Description +TEXT +True +IpAddress +VARCHAR(45) +False +Port +INTERGER +True +Latitude +FLOAT8 +False +Longitude +FLOAT8 +False +Color +VARCHAR(7) +False +AvatarUrl +VARCHAR(512) +True +IsActivc +BOOLEAN +True +Created +TIMESTAMPTZ +False +Modified +TIMESTAMPTZ +False + + • API +Device +API +Method +Routes +Danh sách +GET +/devices +/devices/{DeviceId} +Cập Nhật +PUT +/devices/{DeviceId} +Xóa +DELETE +/devices/{DeviceId} +Upload Avatar +POST +/devices/{DeviceId}/avatar + + + • Cấu Hình Giám Sát (MonitorConfig) + • Mô tả chức năng +Module MonitorConfig quản lý cấu hình phương thức và tần suất kiểm tra trạng thái cho từng thiết bị. Mỗi thiết bị có một bản cấu hình riêng — xác định dùng Ping (ICMP qua icmplib) hay SNMP (qua pysnmp), tần suất kiểm tra (interval), timeout, và các tham số giao thức cụ thể. Toàn bộ xử lý diễn ra trong Python, không phụ thuộc service bên ngoài. + +• Ping (ICMP): dùng icmplib — async_ping() — kiểm tra kết nối cơ bản, đo RTT và packet loss +• SNMP: dùng pysnmp — get_cmd() — lấy thông tin chi tiết qua OID (sysName, sysUpTime, ifNumber,...) +• Hai phương thức có thể bật đồng thời — APScheduler tạo 2 job độc lập cho cùng 1 thiết bị +• Interval cấu hình per-device — thiết bị quan trọng: 30s/lần, thiết bị ít quan trọng: 5 phút/lần +• Thay đổi config → APScheduler.reschedule_job() cập nhật ngay, không cần restart service + +APSscheduler ở đây chính là thư viện Python chạy trong bộ nhớ (RAM) của backend. Dữ liệu để APScheduler biết "cần làm gì, bao lâu 1 lần" → lấy từ bảng MonitorConfig khi app khởi động. + + • Mô tả quy trình +Luồng cấu hình monitoring cho thiết bị: ++ Vào trang chi tiết thiết bị -> Cấu hình giám sát ++ Chọn phương thức: Ping, SNMP hoặc cả hai ++ Cấu hình ping: Số packet(Count), timeout(giây), interval kiểm tra(giây) ++ Cấu hình SNMP: Community String, SNMP Version(v2c/v3), PORT, Danh sách OID tùy chỉnh ++ Lưu config vào DB -> Gọi APSscheduler.add_job() hoặc reschedule_job() tức thì ++ Worker chạy theo interval mới từ chu kỳ tiếp theo + + • Giải pháp và công nghệ + • Ping Worker – icmplib: async_ping(address, count, timeout, privileged=False) + • + + + + + + • CSDL +MonitorConfig +Name +Type +Nullable +ID +UUID +False +DeviceId +UUID +False +EnablePing +BOOLEAN +False +PingCount +SMALLINT +False +PingTimeout +SMALLINT +False +PingInterval +INTEGER +False +EnableSnmp +BOOLEAN +False +SnmpVersion +VARCHAR(5) +True +SnmpCommunity +VARCHAR(256) +True +SnmpPort +INTEGER +True +SnmpInterval +INTEGER +True +SnmpTimeout +SMALLINT +True +SnmpCustomOids +JSONB +True +Created +TIMESTAMPTZ +False +Modified +TIMESTAMPTZ +False + + + • API +API +METHOD +ROUTES +Lấy config của thiết bị +GET +devices/{DeviceId}/monitor-config +Tạo hoặc cập nhật config +PUT +devices/{DevicesId}/monitor-config +TEST kết nối +POST +devices/{DevicesId}/monitor-config/test + + • Lịch sử trạng thái thiết bị(DeviceStatus) + • Mô tả chức năng +Module DeviceStatus lưu trữ kết quả của từng lần Ping/SNMP Worker chạy. Đây là dữ liệu time-series — volume tăng nhanh, cần tối ưu lưu trữ và query. Dữ liệu phục vụ: hiển thị trạng thái realtime trên Leaflet map (bản ghi mới nhất), vẽ biểu đồ response time (Recharts), tính SLA uptime %, và là cơ sở để phát hiện ngưỡng cảnh báo. + + • Mỗi lần Ping Worker chạy → 1 bản ghi mới với status, RTT, packet loss, timestamp + • Mỗi lần SNMP Worker chạy → 1 bản ghi mới với status, dữ liệu OID dạng JSONB + • WebSocket broadcast ngay sau khi lưu — frontend nhận status mới trong vòng < 1 giây + • Chiến lược change-only: chỉ lưu khi status thay đổi so với lần trước — giảm volume đáng kể + + + + + • Mô tả quy trình +Luồng ghi lịch sử trạng thái – Chạy tự động bởi Worker: + • APSscheduler kích hoạt job theo interval đã cấu hình trong MonitorConfig + • Ping Worker: gọi async_ping(ip, count, timeout, privileged=False) → nhận kết quả + • Đánh giá trạng thái: is_alive=True -> up, is_alive=False -> down + • SNMP Worker: gọi get_cmd() cho từng OID trong SnmpCustomOids -> Nhận giá trị + • Đánh giá: Lấy được sysName -> up; timeout/error -> down + • Lưu bản ghi mới vào bảng device_status(Timescale DB) kèm CheckedAt = now(UTC) + • WebSocket gọi ws_manager.broadcast ({type:'device_status', device_id, status, rtt_ms}) + • Kiểm tra AlertConfig: đếm fail liên tiếp -> Nếu vượt FailThreshold -> Gọi Alert Worker + + • Giải pháp và công nghệ + • TimeScaleDB hypertable trên cột CheckedAt – partition tự động theo thời gian, query time- range nhanh + • Retention policy: tự động xóa dữ liệu cũ hơn 90 ngày ( Cấu hình qua TimescaleDb policy) + • Composite index: (DeviceId, CheckedAt DESC) – query lịch sử theo thiết bị trang range + • API time-bucket: group dữ liệu theo 5m/1h/1d tùy range request – dùng TimescaleDB time_bucket() + • Change detection: so sánh với bản ghi gần nhất, chỉ lưu khi status thay đổi hoặc RTT thay đổi >20% + • CSDL +Name +Type +Nullable +ID +BIGSERIAL +False +DeviceId +UUID +False +Status +VARCHAR(20) +False +Method +VARCHAR(10) +False +ResponseTimeMs +FLOAT8 +True +PacketLoss +FLOAT8 +True +SnmpData +JSONB +True +Detail +TEXT +True +CheckedAt +TIMESTAMPTZ +False + + + • API +API +Method +Routes +Lịch sử theo thiết bị +-Filter: ++ DateFrom, DateTo(datetime) ++ Status(up/down/degraded) ++ Method(ping,snmp) ++ Bucket(5m/1h/1d) +-Paging: Page + Size +GET +- devices/{DeviceId}/status/history +- devices/{DeviceId}/status/latest +-> Trả về bản ghi status mới nhất +Trạng thái tất cả các thiết bị +GET +-devices/status/current +Trả về map: {deviceId: {status, checkedAt, responseMs}} + + + + + + • Cảnh báo (Alert) + • Mô tả chức năng +Module Alert gồm hai phần: AlertConfig (cấu hình ngưỡng và kênh cảnh báo device và AlertLog( Lịch sử các sự cố phát sinh). Hệ Thống tự động phát cảnh báo khi Ping/SNMP Worker phát hiện thiết bị vi phạm ngưỡng – thông báo qua WebSocket toast trên giao diện web và email HTML qua STMP ( thư viện của Python smtplib + Jinja2 ) + + • Mô tả quy trình +Luồng phát hiện và gửi cảnh báo – được gọi bởi Ping/SNMP Worker: + • Worker phát hiện thiết bị trả về status = down → gọi check_and_alert(device_id) + • Đếm số lần fail liên tiếp: query bản ghi gần nhất trong DeviceStatus cho đến khi gặp status != down + • So sánh với AlertConfig.FailThreshold — nếu fail_count < threshold → không làm gì + • Kiểm tra cooldown: lấy AlertLog gần nhất, tính elapsed = now - triggered_at (phút) + • Nếu elapsed < CooldownMinutes → bỏ qua, không gửi lại (tránh spam) + • Tạo bản ghi AlertLog mới (status: active) + • Nếu NotifyWeb=True: gọi ws_manager.broadcast() — tất cả client nhận toast đỏ + • Nếu NotifyEmail=True và EmailRecipients không rỗng: gọi send_alert_email() qua smtplib + • Khi thiết bị up lại: Worker tự động cập nhật AlertLog.ResolvedAt, AlertLog.Status = resolved + + • Giải pháp và công nghệ + • Email: smtplib chuẩn Python — hỗ trợ SMTP_SSL, cấu hình host/port/user/pass qua .env + • HTML Email template: Jinja2 (pip install jinja2) — render template với context {device, alert} + • Web alert payload: {type:'alert', device_id, device_name, ip, status, message, triggered_at} + + • CSDL +AlertConfig +Name +Type +Nullable +ID +UUID +False +DeviceId +UUID +False +IsEnabled +BOOLEAN +False +FailThreshold +SMALLINT +False +CooldownMinutes +INTEGER +False +NotifyWeb +BOOLEAN +False +NotifyEmail +BOOLEAN +False +EmailRecipients +TEXT[] +True +Created +TIMESTAMPTZ +False +Modified +TIMESTAMPTZ +False + +AlertLog +Name +Type +Nullable +ID +BIGSERIAL +False +DeviceId +UUID +False +AlertType +VARCHAR(50) +False +Status +VARCHAR(20) +False +Message +TEXT +False +TriggeredAt +TIMESTAMPTZ +False +ResolvedAt +TIMESTAMPTZ +True +AckBy +UUID +True +AckAt +TIMESTAMPTZ +True +EmailSent +BOOLEAN +False +EmailSentAt +TIMESTAMPTZ +True + + + • API +AlertConfig + +API +Method +Routes +Lấy cấu hình cảnh báo +GET +-devices/{DeviceId}/alert-config +Tạo hoặc cập nhật cấu hình +PUT +- devices/{DeviceId}/alert-config +Test gửi email thử +POST +- devices/{DeviceId}/alert-config/test-email + + + AlertLog + +API +Method +Routes +Danh sách cảnh báo +- Paging: Page + Size +- SortBy: ++ TriggeredAt ++ Status +- FilterBy: ++ DeviceId ++ AlertType ++ Status (active/resolved) ++ DateFrom, DateTo ++ EmailSent +GET + • alerts + • devices/{DeviceId}/alerts +Acknowledge cảnh báo +PATCH + • alerts/{AlertId}/acknowledge +Đánh dấu đã giải quyết +PATCH + • alerts/{AlertId}/resolve +Thống kê +GET + • alerts/summary + Trả về : {totalActive, resolvedToday, topDevices} +