From 69cc215954cebf157aa42019f7201be7739fda84 Mon Sep 17 00:00:00 2001 From: drackxuskrax Date: Tue, 21 Jul 2026 16:53:38 -0500 Subject: [PATCH] Initial --- .env | 2 + .env.example | 3 + README.md | 122 ++++ database.sqlite | Bin 0 -> 12288 bytes db.js | 59 ++ package-lock.json | 1659 +++++++++++++++++++++++++++++++++++++++++++++ package.json | 19 + public/app.js | 659 ++++++++++++++++++ public/index.html | 60 ++ public/style.css | 731 ++++++++++++++++++++ schema.sql | 13 + server.js | 151 +++++ 12 files changed, 3478 insertions(+) create mode 100644 .env create mode 100644 .env.example create mode 100644 README.md create mode 100644 database.sqlite create mode 100644 db.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 public/app.js create mode 100644 public/index.html create mode 100644 public/style.css create mode 100644 schema.sql create mode 100644 server.js diff --git a/.env b/.env new file mode 100644 index 0000000..e41ebcc --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +PORT=3000 +DATABASE_FILE=database.sqlite diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..692d2c6 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +PORT=3000 +# SQLite database filename (creates a local file in the project folder) +DATABASE_FILE=database.sqlite diff --git a/README.md b/README.md new file mode 100644 index 0000000..9049fec --- /dev/null +++ b/README.md @@ -0,0 +1,122 @@ +# Claro Cases Tracking System - Manual Técnico + +Este documento detalla el funcionamiento técnico, arquitectura y endpoints de la aplicación **Claro Cases**, un sistema en tiempo real para rastrear solicitudes de clientes, gestionar casos y registrar métricas de tiempo de atención. + +--- + +## 🏗️ Arquitectura del Sistema + +La aplicación sigue una arquitectura desacoplada monolítica simple basada en Node.js, Express y SQLite: + +```mermaid +graph TD + Client[Cliente Externo / API Client] -->|POST /api/requests| ExpressServer[Express Server Node.js] + ExpressServer -->|Guarda Datos| SQLite[SQLite database.sqlite] + ExpressServer -->|SSE Broadcast| Frontend[Frontend SPA HTML/JS] + Frontend -->|Interacción de Operador| ExpressServer +``` + +1. **Frontend (SPA - Single Page Application)**: Desarrollado con Vanilla HTML5, Javascript (ES6) y CSS3 adaptativo con soporte para temas Claro/Oscuro. +2. **Backend**: Servidor REST Express que provee persistencia y notificaciones en tiempo real a través de Server-Sent Events (SSE). +3. **Persistencia**: Base de datos ligera embebida SQLite gestionada eficientemente de manera síncrona por medio del driver `better-sqlite3`. + +--- + +## 🗄️ Esquema de la Base de Datos + +La tabla principal es `requests`. La inicialización del esquema y las migraciones automáticas de columnas se gestionan dinámicamente en el módulo [db.js](file:///c:/Users/pepit/Desktop/doc/NODE/autonomus/Claro%20Cases/db.js). + +```sql +CREATE TABLE IF NOT EXISTS requests ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + description TEXT, + status TEXT DEFAULT 'Pending', + external_id TEXT, + cedula TEXT, + tipo_solicitud TEXT, + payload TEXT, -- Objeto JSON guardado como String + handling_time INTEGER DEFAULT 0, -- Tiempo de gestión en segundos + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +); +``` + +--- + +## 🔌 API Endpoints (Backend) + +### 1. Registrar Nuevo Caso +* **Endpoint**: `POST /api/requests` +* **Content-Type**: `application/json` +* **Payload**: + ```json + { + "title": "Título del Caso", + "description": "Detalle descriptivo", + "external_id": "EXT-12345", + "cedula": "10203040", + "tipo_solicitud": "validacion_usuario", + "payload": { + "nombre": "Juan Pérez", + "telefono": "31000000" + } + } + ``` +* **Comportamiento**: Guarda la solicitud y transmite en tiempo real la información a todos los operadores conectados usando SSE. + +### 2. Stream de Actualizaciones en Tiempo Real +* **Endpoint**: `GET /api/sse` +* **Content-Type**: `text/event-stream` +* **Comportamiento**: Registra el cliente para recibir eventos unidireccionales desde el servidor en tiempo real. + +### 3. Obtener Solicitudes +* **Endpoint**: `GET /api/requests` +* **Comportamiento**: Devuelve todos los casos de la base de datos en orden descendente por fecha de creación. + +### 4. Actualizar Estado / Finalizar Caso +* **Endpoint**: `PUT /api/requests/:id` +* **Payload**: + ```json + { + "status": "Finalizado", + "handling_time": 125, + "payload": { + "valor_cierre": "Comentario de finalización" + } + } + ``` + +### 5. Eliminar Solicitud +* **Endpoint**: `DELETE /api/requests/:id` + +--- + +## ⚡ Lógica del Operador (Frontend) + +El flujo interactivo se gestiona en [public/app.js](file:///c:/Users/pepit/Desktop/doc/NODE/autonomus/Claro%20Cases/public/app.js) y se compone de: + +* **Sincronización en Tiempo Real (SSE)**: Permite la llegada instantánea de nuevos casos al panel de control del operador sin recargar la página. Al ingresar un caso nuevo: + - Se reproduce una alerta de sonido usando la API nativa de **Web Audio**. + - Se genera una **Notificación de Escritorio HTML5** en el sistema operativo. + - El título de la pestaña del navegador destella con la cantidad de casos pendientes. +* **Filtros e Historial**: + - Buscador integrado que busca en tiempo real por título, ID de referencia, descripción, cédula y tipo de solicitud. + - Pestañas rápidas para clasificar casos entre *Todos*, *Pendientes* y *Finalizados*. +* **Flujos Especiales al Gestionar Casos**: + - Al dar clic en **Gestionar Caso**, se inicia un cronómetro individual para medir el tiempo que el operador tarda en resolver la solicitud. + - **Solicitud de Validación**: Si el caso contiene la palabra `"validacion"` en su campo `tipo_solicitud`, la UI cambia dinámicamente y despliega los botones **Sí** y **No** integrados bajo la pregunta *"¿El usuario es válido?"*. Al hacer clic en cualquiera de ellos, el caso se cierra. + - **Otras Solicitudes**: Si es de cualquier otro tipo, la interfaz le exige al operador ingresar un valor de resolución en una entrada de texto antes de dar clic en **Finalizar Caso**. Este valor se inyecta en el objeto JSON de `payload` del caso. + +--- + +## 🛠️ Ejecución Local + +1. Instalar dependencias: + ```cmd + npm install + ``` +2. Ejecutar servidor en modo desarrollo (con recarga automática vía nodemon): + ```cmd + npm run dev + ``` + El servidor levantará en http://localhost:3000. diff --git a/database.sqlite b/database.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..e8b9e651b0efa318713163acb75ce84392074ac8 GIT binary patch literal 12288 zcmeI2&2HO95XUJiNE}si>mK5|j?uMpa12ZCQq)JxQxt|y-AcA1Q$dX)5Z2_{S%Bg) z%aze2C zqC+lvL?T1`NK2)(`v_^8X2YI_J=w0oNxUapDmi)2(bhJ8{53cCuQrooT8_>A^q#t^ zND)v36ahs*5l{pa0YyL&Py`f#%RyjJPu*BpUQWI4NOIs)L7(?&B%|awW3}zNVXddJz_-htbi_Ks74dXh<>BFFyPK|8y! zcN&e@?9s>-ER>7~!@}4WNh15vsP031tKMliXthNHj|E4oaRvIVq#_`G7dnqyjBO!# z;dAol+7*;Y>UH70@gWS|*>2h>Y0T=h+c2zdY}|9|%~qy>Tc3gnjguEMZpSeu-N1d*(-TR@AFVubGvash4zI!Uitm3LaP=D2it(4(312YBjpDGMG1(Q~DF)gA70; zK`fF^)F*=R(?;fj(4dkBHQwkB6lVEE@GSs0unFUVkj!L>|RL!!U^tpOGnR2&3n(rX9IK|}7>FHlqCKVG;n0(gvA<^dD zkR0*B^-p3LOGAOoo*%~8mt0_VluZLC>X-(RS-5hlz<5d(*QR0QZcVA;{BGlds{)&q zs%dN{syOR1SbA0~T1)GzhDD-bfNhCBJs_wwK1K{eun=58UY%~c}AOZTw~8>LqJC` zf?)zNe9<4d`oa9|DQ%yBIsXq)plq0xNq$f`yQG8prK`sTp7)8$Z=Ge51M#Ik{7Zw6 q+7tmrKoL*`6ahs*5l{pa0YyL&Py`eKMc~p9$fnZEspJnPxBmm5kt9R_ literal 0 HcmV?d00001 diff --git a/db.js b/db.js new file mode 100644 index 0000000..f019f14 --- /dev/null +++ b/db.js @@ -0,0 +1,59 @@ +const Database = require('better-sqlite3'); +const fs = require('fs'); +const path = require('path'); +require('dotenv').config(); + +const dbFile = process.env.DATABASE_FILE || 'database.sqlite'; +const dbPath = path.isAbsolute(dbFile) ? dbFile : path.join(__dirname, dbFile); + +let db; +try { + // better-sqlite3 creates the database file synchronously on creation + db = new Database(dbPath); + console.log(`📂 SQLite database connected via better-sqlite3: ${dbPath}`); +} catch (err) { + console.error('💥 Failed to open SQLite database:', err.message); + process.exit(1); +} + +// Wrap operations in async functions to maintain compatibility with server.js +async function query(sql, params = []) { + const stmt = db.prepare(sql); + const trimmedSql = sql.trim().toUpperCase(); + const isMutating = trimmedSql.startsWith('INSERT') || + trimmedSql.startsWith('UPDATE') || + trimmedSql.startsWith('DELETE'); + + if (isMutating) { + const result = stmt.run(params); + return { insertId: result.lastInsertRowid, changes: result.changes }; + } else { + return stmt.all(params); + } +} + +async function initializeSchema() { + try { + const schemaPath = path.join(__dirname, 'schema.sql'); + if (fs.existsSync(schemaPath)) { + const sql = fs.readFileSync(schemaPath, 'utf8'); + // Executes multiple SQL statements in schema.sql + db.exec(sql); + try { + db.exec('ALTER TABLE requests ADD COLUMN tipo_solicitud TEXT;'); + console.log('➕ Added tipo_solicitud column to existing requests table'); + } catch (e) { + // Ignore if column already exists + } + console.log('✅ Database schema initialized/verified successfully'); + } + } catch (error) { + console.error('⚠️ Failed to initialize schema automatically:', error.message); + } +} + +module.exports = { + query, + initializeSchema, + db +}; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..50e07ac --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1659 @@ +{ + "name": "claro-cases", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "claro-cases", + "version": "1.0.0", + "dependencies": { + "better-sqlite3": "^12.11.1", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.19.2" + }, + "devDependencies": { + "nodemon": "^3.1.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/better-sqlite3": { + "version": "12.11.1", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.11.1.tgz", + "integrity": "sha512-dq9AtApgg5PGFtBzPFSBl3HZQjHok5gaQCM6zh2Yk0aSmDCs1CbnVI8/HgASQkNKsWFpseIO9beg5xxpYhbIfA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", + "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.94.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.94.0.tgz", + "integrity": "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/nodemon": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz", + "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^10.2.1", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "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/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "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/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "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": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tar-fs": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", + "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..b3dbfc6 --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "claro-cases", + "version": "1.0.0", + "description": "Claro Cases tracking system with Postgres and live UI updates", + "main": "server.js", + "scripts": { + "start": "node server.js", + "dev": "nodemon server.js" + }, + "dependencies": { + "better-sqlite3": "^12.11.1", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.19.2" + }, + "devDependencies": { + "nodemon": "^3.1.0" + } +} diff --git a/public/app.js b/public/app.js new file mode 100644 index 0000000..d195dbf --- /dev/null +++ b/public/app.js @@ -0,0 +1,659 @@ +// Claro Cases Frontend Controller +let requests = []; +let selectedRequest = null; +let searchQuery = ''; +let currentTab = 'all'; + +// Independent Multi-Timer State +// Structure: { [caseId]: { elapsed: 0, isRunning: false, lastStarted: timestamp } } +let caseTimers = {}; + +// Tab/Title Notification State +let unreadCount = 0; +let titleInterval = null; +let isTabFocused = true; + +// Audio Context State +let audioCtx = null; + +// DOM Elements +const casesList = document.getElementById('cases-list'); +const casesCount = document.getElementById('cases-count'); +const caseDetails = document.getElementById('case-details'); +const searchInput = document.getElementById('search-input'); +const connectionStatus = document.getElementById('connection-status'); +const connectionText = document.getElementById('connection-text'); + +// Initialize +window.addEventListener('DOMContentLoaded', () => { + loadTimers(); + fetchRequests(); + setupSSE(); + setupSearch(); + setupTheme(); + setupTabs(); + setupWindowFocusListeners(); + startGlobalInterval(); + + // Request desktop notification permissions + if (window.Notification && Notification.permission === 'default') { + Notification.requestPermission(); + } +}); + +// Window Focus Listeners to clear unread counts +function setupWindowFocusListeners() { + window.addEventListener('focus', () => { + isTabFocused = true; + unreadCount = 0; + stopTitleFlashing(); + document.title = 'Claro Cases Dashboard'; + }); + + window.addEventListener('blur', () => { + isTabFocused = false; + }); + + // Enable AudioContext on first click/keypress gesture to bypass Chrome's autoplay policies + const resumeAudio = () => { + if (!audioCtx) { + audioCtx = new (window.AudioContext || window.webkitAudioContext)(); + } + if (audioCtx.state === 'suspended') { + audioCtx.resume(); + } + }; + document.addEventListener('click', resumeAudio); + document.addEventListener('keydown', resumeAudio); +} + +// Load case timers from localStorage and calculate offset elapsed time for running ones +function loadTimers() { + const saved = localStorage.getItem('caseTimers'); + if (saved) { + try { + caseTimers = JSON.parse(saved); + // For any running timer, calculate elapsed time since last reload + for (const [id, timer] of Object.entries(caseTimers)) { + if (timer.isRunning) { + const timeDiff = Math.floor((Date.now() - timer.lastStarted) / 1000); + timer.elapsed += (timeDiff > 0 ? timeDiff : 0); + timer.lastStarted = Date.now(); // reset start mark to now + } + } + saveTimers(); + } catch (e) { + console.error('Failed to parse saved timers:', e); + caseTimers = {}; + } + } +} + +// Save case timers to localStorage +function saveTimers() { + localStorage.setItem('caseTimers', JSON.stringify(caseTimers)); +} + +// Fetch all existing requests +async function fetchRequests() { + try { + const response = await fetch('/api/requests'); + if (!response.ok) throw new Error('Error al obtener solicitudes'); + requests = await response.json(); + renderList(); + } catch (error) { + console.error('Fetch error:', error); + casesList.innerHTML = `
Error al cargar solicitudes. Verifica la base de datos.
`; + } +} + +// Connect to Server-Sent Events (SSE) for real-time updates +function setupSSE() { + const sse = new EventSource('/api/sse'); + + sse.onopen = () => { + connectionStatus.className = 'status-dot connected'; + connectionText.textContent = 'En línea'; + }; + + sse.onerror = (error) => { + console.error('SSE Error:', error); + connectionStatus.className = 'status-dot disconnected'; + connectionText.textContent = 'Reconectando...'; + }; + + sse.onmessage = (event) => { + try { + const newRequest = JSON.parse(event.data); + // Prepend new request to local state + requests.unshift(newRequest); + renderList(); + + // Play alert chime, send desktop notification and flash tab title + playNotificationSound(); + showDesktopNotification(newRequest); + triggerTitleNotification(); + + console.log('🔔 Nueva solicitud recibida:', newRequest); + } catch (err) { + console.error('Error parsing SSE data:', err); + } + }; +} + +// Search Filter +function setupSearch() { + searchInput.addEventListener('input', (e) => { + searchQuery = e.target.value.toLowerCase().trim(); + renderList(); + }); +} + +// Render Request Cards List +function renderList() { + const filtered = requests.filter(req => { + // Tab filter + if (currentTab === 'pending' && req.status.toLowerCase() === 'finalizado') { + return false; + } + if (currentTab === 'finalizado' && req.status.toLowerCase() !== 'finalizado') { + return false; + } + + const titleMatch = req.title.toLowerCase().includes(searchQuery); + const extIdMatch = req.external_id && req.external_id.toLowerCase().includes(searchQuery); + const descMatch = req.description && req.description.toLowerCase().includes(searchQuery); + const cedulaMatch = req.cedula && req.cedula.toLowerCase().includes(searchQuery); + const tipoMatch = req.tipo_solicitud && req.tipo_solicitud.toLowerCase().includes(searchQuery); + return titleMatch || extIdMatch || descMatch || cedulaMatch || tipoMatch; + }); + + casesCount.textContent = filtered.length; + + if (filtered.length === 0) { + casesList.innerHTML = `
No se encontraron solicitudes.
`; + return; + } + + casesList.innerHTML = ''; + filtered.forEach(req => { + const card = document.createElement('div'); + const isActive = selectedRequest && selectedRequest.id === req.id; + card.className = `case-card ${isActive ? 'active' : ''}`; + + const formattedDate = new Date(req.created_at).toLocaleString(); + const statusClass = `status-${req.status.toLowerCase()}`; + + // Get time from this case's timer if active + const timer = caseTimers[req.id]; + let timerTag = ''; + if (timer) { + const elapsedTotal = timer.elapsed + (timer.isRunning ? Math.floor((Date.now() - timer.lastStarted) / 1000) : 0); + timerTag = ` ⏳ ${formatTime(elapsedTotal)}`; + } + + card.innerHTML = ` +
+ ${escapeHTML(req.title)}${timerTag} + ${escapeHTML(req.status)} +
+

${escapeHTML(req.description || 'Sin descripción')}

+ + `; + + card.addEventListener('click', () => { + document.querySelectorAll('.case-card').forEach(c => c.classList.remove('active')); + card.classList.add('active'); + + selectedRequest = req; + renderDetails(); + }); + + casesList.appendChild(card); + }); +} + +// Render Selected Request Details +function renderDetails() { + if (!selectedRequest) { + caseDetails.innerHTML = ` +
+
📁
+

Selecciona una solicitud

+

Haz clic en cualquier elemento de la lista de la izquierda para ver su información detallada y acciones de gestión.

+
+ `; + return; + } + + const formattedDate = new Date(selectedRequest.created_at).toLocaleString(); + const statusClass = `status-${selectedRequest.status.toLowerCase()}`; + + // Parse Payload JSON + let payloadObj = selectedRequest.payload; + if (typeof payloadObj === 'string') { + try { + payloadObj = JSON.parse(payloadObj); + } catch (e) { + payloadObj = {}; + } + } + + // Try to find cedula in root or payload fallbacks + let displayCedula = selectedRequest.cedula; + if (!displayCedula && payloadObj) { + displayCedula = payloadObj.cedula || payloadObj.cédula || payloadObj.documento || payloadObj.identification || payloadObj.id || payloadObj.cc; + } + + // Generate HTML for payload fields + let payloadFieldsHTML = ''; + if (payloadObj && Object.keys(payloadObj).length > 0) { + payloadFieldsHTML = '
'; + for (const [key, value] of Object.entries(payloadObj)) { + const displayValue = typeof value === 'object' ? JSON.stringify(value) : value; + payloadFieldsHTML += ` +
+ ${escapeHTML(key)} + ${escapeHTML(displayValue)} +
+ `; + } + payloadFieldsHTML += '
'; + } else { + payloadFieldsHTML = '
Sin datos adicionales
'; + } + + // Interactive buttons and status management panel HTML + let actionsPanelHTML = ''; + const isFinalizado = selectedRequest.status.toLowerCase() === 'finalizado'; + const timer = caseTimers[selectedRequest.id]; + + if (isFinalizado) { + const formattedHandlingTime = formatSavedTime(selectedRequest.handling_time); + actionsPanelHTML = ` +
+
+ Tiempo de Gestión: + ${formattedHandlingTime} +
+ +
+ `; + } else if (timer && timer.isRunning) { + const elapsedTotal = timer.elapsed + Math.floor((Date.now() - timer.lastStarted) / 1000); + const isValidation = selectedRequest.tipo_solicitud && + selectedRequest.tipo_solicitud.toLowerCase().includes('validacion'); + actionsPanelHTML = ` +
+
+ ⏳ Tiempo transcurrido: + ${formatTime(elapsedTotal)} +
+ ${isValidation ? ` +
+ ¿El usuario es válido? +
+ + +
+
+ ` : ` +
+ Ingrese el valor de resolución: + +
+ + +
+
+ `} +
+ `; + } else { + // Show total handling time if there was some time saved, otherwise none + const timeDisplay = selectedRequest.handling_time ? ` +
+ Último tiempo guardado: + ${formatSavedTime(selectedRequest.handling_time)} +
+ ` : ''; + + actionsPanelHTML = ` +
+ ${timeDisplay} +
+ + +
+
+ `; + } + + caseDetails.innerHTML = ` +
+
+
+ ${escapeHTML(selectedRequest.status)} + Creado el ${formattedDate} +
+

${escapeHTML(selectedRequest.title)}

+ + +
+ + +
+

Panel de Operación

+ ${actionsPanelHTML} +
+ +
+

Descripción

+

${escapeHTML(selectedRequest.description || 'No se proporcionó una descripción.')}

+
+ +
+

Datos de la Solicitud

+ ${payloadFieldsHTML} +
+
+ `; +} + +// Timer Functions +function startTimer(requestId) { + // Initialize timer entry if not existing + if (!caseTimers[requestId]) { + caseTimers[requestId] = { + elapsed: 0, + isRunning: false, + lastStarted: 0 + }; + } + + const timer = caseTimers[requestId]; + timer.isRunning = true; + timer.lastStarted = Date.now(); + saveTimers(); + + // Re-render UI to show ticking elements + renderDetails(); + renderList(); +} + +// Global ticking function running once per second for all active timers +let globalInterval = null; +function startGlobalInterval() { + if (globalInterval) clearInterval(globalInterval); + globalInterval = setInterval(() => { + for (const [id, timer] of Object.entries(caseTimers)) { + if (timer.isRunning) { + const timeDiff = Math.floor((Date.now() - timer.lastStarted) / 1000); + const currentTotal = timer.elapsed + timeDiff; + + // Update list card if visible + const cardTimer = document.getElementById(`card-timer-${id}`); + if (cardTimer) { + cardTimer.textContent = `⏳ ${formatTime(currentTotal)}`; + } + + // Update active details stopwatch if selected + if (selectedRequest && selectedRequest.id === parseInt(id)) { + const activeTimer = document.getElementById('active-timer'); + if (activeTimer) { + activeTimer.textContent = formatTime(currentTotal); + } + } + } + } + }, 1000); +} + +function formatTime(totalSeconds) { + const mins = Math.floor(totalSeconds / 60); + const secs = totalSeconds % 60; + return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; +} + +function formatSavedTime(totalSeconds) { + if (!totalSeconds) return '0s'; + const mins = Math.floor(totalSeconds / 60); + const secs = totalSeconds % 60; + return mins > 0 ? `${mins}m ${secs}s` : `${secs}s`; +} + +async function finalizeCase(requestId, additionalPayload = null) { + const timer = caseTimers[requestId]; + let finalTime = 0; + + if (timer) { + if (timer.isRunning) { + const timeDiff = Math.floor((Date.now() - timer.lastStarted) / 1000); + finalTime = timer.elapsed + timeDiff; + } else { + finalTime = timer.elapsed; + } + // Delete timer entry from tracking + delete caseTimers[requestId]; + saveTimers(); + } + + try { + const bodyData = { status: 'Finalizado', handling_time: finalTime }; + if (additionalPayload) { + const currentReq = requests.find(r => r.id === requestId); + let payloadObj = {}; + if (currentReq && currentReq.payload) { + try { + payloadObj = typeof currentReq.payload === 'string' ? JSON.parse(currentReq.payload) : currentReq.payload; + } catch (e) { + payloadObj = {}; + } + } + bodyData.payload = { ...payloadObj, ...additionalPayload }; + } + + const response = await fetch(`/api/requests/${requestId}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(bodyData) + }); + if (!response.ok) throw new Error('Error al finalizar caso'); + const updated = await response.json(); + + // Update local state list + const index = requests.findIndex(r => r.id === requestId); + if (index !== -1) { + requests[index] = updated; + } + selectedRequest = updated; + renderList(); + renderDetails(); + } catch (error) { + console.error(error); + alert('Error al actualizar el estado del caso en el servidor.'); + } +} + +function submitResolution(requestId) { + const input = document.getElementById(`resolution-input-${requestId}`); + const val = input ? input.value.trim() : ''; + finalizeCase(requestId, { valor_cierre: val || 'No especificado' }); +} + +async function deleteCase(requestId) { + if (!confirm('¿Estás seguro de que deseas eliminar esta solicitud?')) return; + + // Clear timer tracking for this case + if (caseTimers[requestId]) { + delete caseTimers[requestId]; + saveTimers(); + } + + try { + const response = await fetch(`/api/requests/${requestId}`, { + method: 'DELETE' + }); + if (!response.ok) throw new Error('Error al eliminar'); + + // Remove from local state + requests = requests.filter(r => r.id !== requestId); + selectedRequest = null; + renderList(); + renderDetails(); + } catch (error) { + console.error(error); + alert('Error al eliminar la solicitud.'); + } +} + +// Helper to escape HTML tags +function escapeHTML(str) { + if (!str) return ''; + return str.replace(/[&<>'"]/g, + tag => ({ + '&': '&', + '<': '<', + '>': '>', + "'": ''', + '"': '"' + }[tag] || tag) + ); +} + +// Theme Switcher +function setupTheme() { + const themeToggle = document.getElementById('theme-toggle'); + + // Check theme settings in local storage + const currentTheme = localStorage.getItem('theme') || 'light'; + if (currentTheme === 'dark') { + document.body.classList.add('dark-mode'); + themeToggle.textContent = '☀️'; + } else { + themeToggle.textContent = '🌙'; + } + + themeToggle.addEventListener('click', () => { + document.body.classList.toggle('dark-mode'); + const isDark = document.body.classList.contains('dark-mode'); + themeToggle.textContent = isDark ? '☀️' : '🌙'; + localStorage.setItem('theme', isDark ? 'dark' : 'light'); + }); +} + +// Tab Switcher +function setupTabs() { + const tabs = document.querySelectorAll('.tab-btn'); + tabs.forEach(tab => { + tab.addEventListener('click', () => { + tabs.forEach(t => t.classList.remove('active')); + tab.classList.add('active'); + currentTab = tab.dataset.tab; + renderList(); + }); + }); +} + +// Play notification sound using Web Audio API +function playNotificationSound() { + try { + // If not initialized yet due to autoplay limits + if (!audioCtx) { + audioCtx = new (window.AudioContext || window.webkitAudioContext)(); + } + + // If it's still suspended (needs user gesture first) + if (audioCtx.state === 'suspended') { + console.warn('AudioContext is suspended. Click on the dashboard page first to enable sound alerts.'); + return; + } + + const now = audioCtx.currentTime; + + const osc = audioCtx.createOscillator(); + const gain = audioCtx.createGain(); + + osc.type = 'sine'; + osc.connect(gain); + gain.connect(audioCtx.destination); + + // Play a friendly two-tone notification sound (chime) + osc.frequency.setValueAtTime(523.25, now); // Tone C5 + osc.frequency.setValueAtTime(659.25, now + 0.12); // Tone E5 + + gain.gain.setValueAtTime(0.08, now); + gain.gain.exponentialRampToValueAtTime(0.001, now + 0.45); + + osc.start(now); + osc.stop(now + 0.5); + } catch (error) { + console.warn('AudioContext is blocked or unsupported:', error); + } +} + +// Show HTML5 desktop notification +function showDesktopNotification(request) { + if (!window.Notification) return; + + if (Notification.permission === 'granted') { + const title = `Claro Cases: ${request.title}`; + const options = { + body: request.description || `ID Referencia: ${request.external_id || '#' + request.id}`, + icon: 'favicon.ico' + }; + + const notification = new Notification(title, options); + + // Clicking the notification automatically selects the request in the UI + notification.onclick = () => { + window.focus(); + selectedRequest = request; + renderList(); + renderDetails(); + }; + } +} + +// Tab Title Flashing Alert (Unread cases) +function triggerTitleNotification() { + if (isTabFocused) return; + unreadCount++; + startTitleFlashing(); +} + +function startTitleFlashing() { + if (titleInterval) clearInterval(titleInterval); + + let showAlt = false; + titleInterval = setInterval(() => { + showAlt = !showAlt; + document.title = showAlt + ? `(🔔 ${unreadCount}) ¡Nuevo Caso!` + : `(${unreadCount}) Claro Cases Dashboard`; + }, 1000); +} + +function stopTitleFlashing() { + if (titleInterval) { + clearInterval(titleInterval); + titleInterval = null; + } +} diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..c2e8bd2 --- /dev/null +++ b/public/index.html @@ -0,0 +1,60 @@ + + + + + + Claro Cases Dashboard + + + + + + +
+
+
+ 🔴 +

Claro Cases

+ En vivo +
+
+ + Desconectado + +
+
+ +
+ +
+ + +
+ + + +
+
+
Cargando solicitudes...
+
+
+ + +
+
+
📁
+

Selecciona una solicitud

+

Haz clic en cualquier elemento de la lista de la izquierda para ver su información detallada y carga útil (payload).

+
+
+
+
+ + + + diff --git a/public/style.css b/public/style.css new file mode 100644 index 0000000..6f5bf91 --- /dev/null +++ b/public/style.css @@ -0,0 +1,731 @@ +/* Claro Cases Styling - Replicating Autonomus Brand Line */ + +:root { + --bg-base: #f0f2f5; + --bg-surface: #ffffff; + --bg-elevated: #f8fafc; + --bg-hover: #e2e8f0; + --border: rgba(0, 0, 0, 0.08); + --border-accent: rgba(255, 78, 0, 0.25); + + --text-primary: #1e293b; + --text-secondary: #475569; + --text-muted: #94a3b8; + + --accent-orange: #ff4e00; + --accent-yellow: #ffa600; + --accent-red: #f80018; + --accent-green: #10b981; + + --gradient-a: linear-gradient(135deg, #ff4e00, #ffa600); + --gradient-b: linear-gradient(135deg, #f80018, #ff4e00); + + --radius-sm: 4px; + --radius-md: 8px; + --radius-lg: 12px; + --radius-xl: 16px; + + --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08); + --shadow-lg: 0 12px 24px rgba(0, 0, 0, 0.12); + + --transition: 0.18s cubic-bezier(0.4, 0, 0.2, 1); + --font-family: 'Inter', system-ui, sans-serif; +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: var(--font-family); + background-color: var(--bg-base); + color: var(--text-primary); + height: 100vh; + overflow: hidden; + font-size: 13px; +} + +.app-container { + display: flex; + flex-direction: column; + height: 100vh; +} + +/* Header */ +.app-header { + height: 50px; + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 16px; + border-bottom: 1px solid var(--border); + background: var(--bg-surface); + z-index: 10; + box-shadow: var(--shadow-sm); +} + +.logo-area { + display: flex; + align-items: center; + gap: 8px; +} + +.logo-icon { + font-size: 1.1rem; +} + +.logo-area h1 { + font-size: 13px; + font-weight: 600; + background: var(--gradient-a); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + letter-spacing: 0.5px; +} + +.live-badge { + background: rgba(16, 185, 129, 0.1); + color: var(--accent-green); + border: 1px solid rgba(16, 185, 129, 0.2); + font-size: 10px; + padding: 1px 6px; + border-radius: 20px; + font-weight: 600; + text-transform: uppercase; +} + +.status-area { + display: flex; + align-items: center; + gap: 6px; + font-size: 11px; + color: var(--text-secondary); +} + +.status-dot { + width: 6px; + height: 6px; + border-radius: 50%; +} + +.status-dot.connected { + background-color: var(--accent-green); + box-shadow: 0 0 6px var(--accent-green); +} + +.status-dot.disconnected { + background-color: var(--accent-red); + box-shadow: 0 0 6px var(--accent-red); +} + +/* Main Layout */ +.app-main { + flex: 1; + display: flex; + overflow: hidden; +} + +/* Sidebar List */ +.cases-sidebar { + width: 320px; + background: var(--bg-surface); + border-right: 1px solid var(--border); + display: flex; + flex-direction: column; + z-index: 5; +} + +.sidebar-header { + padding: 16px; + display: flex; + justify-content: space-between; + align-items: center; +} + +.sidebar-header h2 { + font-size: 12px; + font-weight: 600; + color: var(--text-primary); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.cases-count { + background: var(--bg-base); + padding: 2px 8px; + border-radius: var(--radius-sm); + font-size: 11px; + color: var(--text-secondary); + font-weight: 600; +} + +.search-bar { + padding: 0 16px 12px; +} + +.search-bar input { + width: 100%; + padding: 8px 12px; + background: var(--bg-elevated); + border: 1px solid var(--border); + border-radius: var(--radius-md); + color: var(--text-primary); + font-family: var(--font-family); + font-size: 12px; + outline: none; + transition: var(--transition); +} + +.search-bar input:focus { + border-color: var(--accent-orange); + box-shadow: 0 0 0 3px rgba(255, 78, 0, 0.1); +} + +.tabs-container { + display: flex; + gap: 6px; + padding: 0 16px 12px; + border-bottom: 1px solid var(--border); + margin-bottom: 12px; +} + +.tab-btn { + flex: 1; + background: var(--bg-elevated); + border: 1px solid var(--border); + border-radius: var(--radius-md); + padding: 6px 0; + font-size: 11px; + font-weight: 500; + font-family: var(--font-family); + color: var(--text-secondary); + cursor: pointer; + transition: var(--transition); + text-align: center; +} + +.tab-btn:hover { + background: var(--bg-hover); + color: var(--text-primary); +} + +.tab-btn.active { + background: rgba(255, 78, 0, 0.08); + color: var(--accent-orange); + border-color: var(--accent-orange); + font-weight: 600; +} + +body.dark-mode .tab-btn.active { + background: rgba(255, 78, 0, 0.15); +} + +.cases-list { + flex: 1; + overflow-y: auto; + padding: 0 16px 16px; + display: flex; + flex-direction: column; + gap: 8px; +} + +/* Case Card */ +.case-card { + background: var(--bg-elevated); + border: 1px solid var(--border); + border-radius: var(--radius-md); + padding: 12px; + cursor: pointer; + transition: var(--transition); + position: relative; + overflow: hidden; + animation: slideIn 0.25s ease-out; +} + +.case-card:hover { + background: var(--bg-hover); + border-color: rgba(0, 0, 0, 0.15); +} + +.case-card.active { + background: rgba(255, 78, 0, 0.04); + border-color: var(--accent-orange); +} + +.case-card-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 8px; + margin-bottom: 4px; +} + +.case-title { + font-weight: 600; + font-size: 12px; + line-height: 1.3; + color: var(--text-primary); +} + +.case-status-badge { + font-size: 9px; + padding: 2px 6px; + border-radius: 10px; + font-weight: 600; + text-transform: uppercase; + flex-shrink: 0; +} + +.timing-badge { + font-size: 9px; + font-weight: 600; + color: var(--accent-orange); + background: rgba(255, 78, 0, 0.08); + padding: 1px 5px; + border-radius: 4px; + margin-left: 4px; +} + +/* Status-specific Badges */ +.status-pending { background: rgba(255, 166, 0, 0.12); color: var(--accent-yellow); } +.status-resolved { background: rgba(16, 185, 129, 0.12); color: var(--accent-green); } +.status-failed { background: rgba(248, 0, 24, 0.1); color: var(--accent-red); } +.status-finalizado { background: rgba(16, 185, 129, 0.12); color: var(--accent-green); } + +.case-desc-preview { + font-size: 11px; + color: var(--text-secondary); + margin-bottom: 8px; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + line-height: 1.4; +} + +.case-footer { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 10px; + color: var(--text-muted); + border-top: 1px solid var(--border); + padding-top: 6px; +} + +.case-id { + font-family: monospace; + background: var(--bg-hover); + padding: 1px 4px; + border-radius: 2px; +} + +/* Right Detail Panel */ +.case-details { + flex: 1; + background: var(--bg-surface); + padding: 30px; + overflow-y: auto; + display: flex; + flex-direction: column; +} + +/* Empty State */ +.empty-state { + margin: auto; + text-align: center; + max-width: 380px; +} + +.empty-icon { + font-size: 3rem; + margin-bottom: 16px; + opacity: 0.4; +} + +.empty-state h3 { + font-size: 14px; + margin-bottom: 8px; + font-weight: 600; +} + +.empty-state p { + color: var(--text-secondary); + line-height: 1.6; + font-size: 12px; +} + +/* Full Detail View Layout */ +.detail-view { + animation: fadeIn 0.3s ease; + display: flex; + flex-direction: column; + gap: 24px; + max-width: 800px; + margin: 0 auto; + width: 100%; +} + +.detail-header { + border-bottom: 1px solid var(--border); + padding-bottom: 16px; +} + +.detail-meta { + display: flex; + gap: 12px; + align-items: center; + margin-bottom: 12px; +} + +.detail-title { + font-size: 18px; + font-weight: 700; + line-height: 1.3; + color: var(--text-primary); + margin-bottom: 8px; +} + +.detail-time { + font-size: 11px; + color: var(--text-muted); +} + +.header-metadata-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; + background: var(--bg-base); + border: 1px solid var(--border); + border-radius: var(--radius-md); + padding: 12px; + margin-top: 12px; +} + +.meta-item { + display: flex; + flex-direction: column; + gap: 4px; +} + +.meta-label { + font-size: 11px; + color: var(--text-secondary); + font-weight: 500; +} + +.meta-value { + font-size: 13px; + color: var(--text-primary); +} + +.highlight-meta { + color: var(--accent-orange); + font-weight: 600; +} + +/* Operation / Timer Panel */ +.actions-panel { + display: flex; + justify-content: space-between; + align-items: center; + background: var(--bg-elevated); + border: 1px solid var(--border); + border-radius: var(--radius-md); + padding: 16px; + gap: 16px; +} + +.actions-panel.active-panel { + border-color: var(--accent-orange); + background: rgba(255, 78, 0, 0.02); +} + +.actions-panel.finished-panel { + border-color: var(--accent-green); + background: rgba(16, 185, 129, 0.02); +} + +.timer-display { + display: flex; + flex-direction: column; + gap: 4px; +} + +.timer-lbl { + font-size: 11px; + color: var(--text-secondary); + font-weight: 500; +} + +.timer-val { + font-size: 20px; + font-weight: 700; + font-family: monospace; + color: var(--text-primary); +} + +.active-panel .timer-val { + color: var(--accent-orange); +} + +.finished-panel .timer-val { + color: var(--accent-green); +} + +.button-group { + display: flex; + gap: 10px; + align-items: center; +} + +/* Buttons */ +.btn { + padding: 8px 16px; + font-size: 12px; + font-weight: 600; + font-family: var(--font-family); + border: none; + border-radius: var(--radius-md); + cursor: pointer; + transition: var(--transition); +} + +.btn-primary { + background: var(--accent-orange); + color: white; +} + +.btn-primary:hover { + background: #e04400; +} + +.btn-success { + background: var(--accent-green); + color: white; +} + +.btn-success:hover { + background: #0d9668; +} + +.btn-danger { + background: transparent; + color: var(--accent-red); + border: 1px solid rgba(248, 0, 24, 0.2); +} + +.btn-danger:hover { + background: rgba(248, 0, 24, 0.06); + border-color: var(--accent-red); +} + +.detail-section h4 { + font-size: 11px; + font-weight: 600; + margin-bottom: 8px; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.detail-section p.description-text { + font-size: 12px; + line-height: 1.6; + color: var(--text-primary); + background: var(--bg-elevated); + border: 1px solid var(--border); + padding: 16px; + border-radius: var(--radius-md); +} + +/* Info Grid (Payload Key-Value) */ +.info-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 12px; +} + +.info-item { + background: var(--bg-elevated); + border: 1px solid var(--border); + border-radius: var(--radius-md); + padding: 12px; + display: flex; + flex-direction: column; + gap: 6px; +} + +.info-label { + font-size: 10px; + color: var(--text-secondary); + text-transform: uppercase; + font-weight: 600; + letter-spacing: 0.3px; +} + +.info-value { + font-size: 12px; + color: var(--text-primary); + word-break: break-all; + font-weight: 500; +} + +.no-payload { + color: var(--text-muted); + font-style: italic; + padding: 8px 0; +} + +/* Custom Scrollbars */ +::-webkit-scrollbar { + width: 5px; + height: 5px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: var(--bg-hover); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--text-muted); +} + +/* Placeholders */ +.loading-placeholder { + text-align: center; + color: var(--text-secondary); + padding: 30px 0; +} + +/* Animations */ +@keyframes slideIn { + from { transform: translateY(8px); opacity: 0; } + to { transform: translateY(0); opacity: 1; } +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +.animate-pulse { + animation: pulse-op 1.5s infinite; +} + +@keyframes pulse-op { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + +/* Theme Toggle Button */ +.theme-toggle-btn { + background: transparent; + border: 1px solid var(--border); + font-size: 14px; + padding: 4px 8px; + border-radius: var(--radius-md); + cursor: pointer; + margin-left: 12px; + transition: var(--transition); + display: flex; + align-items: center; + justify-content: center; + color: var(--text-primary); +} + +.theme-toggle-btn:hover { + background: var(--bg-hover); + border-color: var(--text-muted); +} + +/* Dark Mode Overrides */ +body.dark-mode { + --bg-base: #0c0d14; + --bg-surface: #141622; + --bg-elevated: #1d2030; + --bg-hover: #2b2f46; + --border: rgba(255, 255, 255, 0.08); + + --text-primary: #f1f5f9; + --text-secondary: #94a3b8; + --text-muted: #64748b; +} + +body.dark-mode ::-webkit-scrollbar-thumb { + background: var(--bg-hover); +} + +/* Request Type Badges */ +.case-type-badge { + background: rgba(255, 78, 0, 0.1); + color: var(--accent-orange); + border: 1px solid rgba(255, 78, 0, 0.25); + font-size: 10px; + padding: 1px 6px; + border-radius: 4px; + font-weight: 600; + text-transform: uppercase; + display: inline-block; + white-space: nowrap; +} + +/* Custom Modal Dialog */ +.modal-overlay { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: rgba(0, 0, 0, 0.4); + backdrop-filter: blur(4px); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + animation: fadeIn 0.2s ease-out; +} + +.modal-content { + background: var(--bg-surface); + border: 1px solid var(--border); + padding: 24px; + border-radius: var(--radius-lg); + box-shadow: var(--shadow-lg); + max-width: 400px; + width: 90%; + text-align: center; + animation: slideIn 0.2s ease-out; +} + +.modal-title { + font-size: 1.1rem; + font-weight: 600; + margin-bottom: 12px; + color: var(--text-primary); +} + +.modal-message { + font-size: 13px; + color: var(--text-secondary); + margin-bottom: 24px; +} + +.modal-buttons { + display: flex; + gap: 12px; + justify-content: center; +} + +.modal-buttons .btn { + padding: 8px 24px; +} diff --git a/schema.sql b/schema.sql new file mode 100644 index 0000000..08fbea5 --- /dev/null +++ b/schema.sql @@ -0,0 +1,13 @@ +-- Table structure for Claro Cases requests (SQLite) +CREATE TABLE IF NOT EXISTS requests ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + description TEXT, + status TEXT DEFAULT 'Pending', + external_id TEXT, + cedula TEXT, + tipo_solicitud TEXT, + payload TEXT, + handling_time INTEGER DEFAULT 0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +); diff --git a/server.js b/server.js new file mode 100644 index 0000000..1fdcf92 --- /dev/null +++ b/server.js @@ -0,0 +1,151 @@ +const express = require('express'); +const cors = require('cors'); +const path = require('path'); +const db = require('./db'); +require('dotenv').config(); + +const app = express(); +const PORT = process.env.PORT || 3000; + +app.use(cors()); +app.use(express.json()); +app.use(express.static(path.join(__dirname, 'public'))); + +// Store active Server-Sent Events (SSE) clients +let sseClients = []; + +// SSE Registration Endpoint +app.get('/api/sse', (req, res) => { + res.setHeader('Content-Type', 'text/event-stream'); + res.setHeader('Cache-Control', 'no-cache'); + res.setHeader('Connection', 'keep-alive'); + res.flushHeaders(); + + sseClients.push(res); + console.log(`🔌 Client connected to SSE stream. Total clients: ${sseClients.length}`); + + req.on('close', () => { + sseClients = sseClients.filter(client => client !== res); + console.log(`🔌 Client disconnected. Total clients: ${sseClients.length}`); + }); +}); + +// Function to broadcast new request details to all connected SSE clients +function broadcastNewRequest(request) { + const data = JSON.stringify(request); + sseClients.forEach(client => { + client.write(`data: ${data}\n\n`); + }); +} + +// API: Get all requests (ordered by newest first) +app.get('/api/requests', async (req, res) => { + try { + const rows = await db.query('SELECT * FROM requests ORDER BY created_at DESC'); + res.json(rows); + } catch (error) { + console.error('Error fetching requests:', error); + res.status(500).json({ error: 'Database query failed' }); + } +}); + +// API: Post new request (receives external data) +app.post('/api/requests', async (req, res) => { + const { title, description, external_id, status, payload, cedula } = req.body; + const tipo_solicitud = req.body.tipo_solicitud || req.body['Tipo de solicitud'] || req.body.tipo_de_solicitud || null; + + if (!title) { + return res.status(400).json({ error: 'Title is required' }); + } + + try { + const queryText = ` + INSERT INTO requests (title, description, external_id, status, cedula, tipo_solicitud, payload) + VALUES (?, ?, ?, ?, ?, ?, ?) + `; + + const parsedPayload = payload ? (typeof payload === 'object' ? payload : { raw: payload }) : {}; + + const values = [ + title, + description || '', + external_id || null, + status || 'Pending', + cedula || null, + tipo_solicitud, + JSON.stringify(parsedPayload) + ]; + + const result = await db.query(queryText, values); + const insertId = result.insertId; + + // Fetch the newly inserted record to broadcast + const rows = await db.query('SELECT * FROM requests WHERE id = ?', [insertId]); + const newRequest = rows[0]; + + // Broadcast the new request to UI clients in real time + broadcastNewRequest(newRequest); + + res.status(201).json(newRequest); + } catch (error) { + console.error('Error saving new request:', error); + res.status(500).json({ error: 'Database insertion failed' }); + } +}); + +// API: Update request (save status, handling time, and optionally payload) +app.put('/api/requests/:id', async (req, res) => { + const { id } = req.params; + const { status, handling_time, payload } = req.body; + + try { + if (payload !== undefined) { + const parsedPayload = typeof payload === 'object' ? payload : { raw: payload }; + await db.query( + 'UPDATE requests SET status = ?, handling_time = ?, payload = ? WHERE id = ?', + [status || 'Pending', handling_time || 0, JSON.stringify(parsedPayload), id] + ); + } else { + await db.query( + 'UPDATE requests SET status = ?, handling_time = ? WHERE id = ?', + [status || 'Pending', handling_time || 0, id] + ); + } + + const rows = await db.query('SELECT * FROM requests WHERE id = ?', [id]); + if (rows.length === 0) { + return res.status(404).json({ error: 'Request not found' }); + } + + res.json(rows[0]); + } catch (error) { + console.error('Error updating request:', error); + res.status(500).json({ error: 'Database update failed' }); + } +}); + +// API: Delete request +app.delete('/api/requests/:id', async (req, res) => { + const { id } = req.params; + + try { + await db.query('DELETE FROM requests WHERE id = ?', [id]); + res.json({ success: true, message: 'Request deleted successfully' }); + } catch (error) { + console.error('Error deleting request:', error); + res.status(500).json({ error: 'Database deletion failed' }); + } +}); + +// Start server after ensuring DB connection and schema setup +async function startServer() { + await db.initializeSchema(); + + app.listen(PORT, () => { + console.log(`🚀 Claro Cases server running at http://localhost:${PORT}`); + }); +} + +startServer().catch(err => { + console.error('💥 Server startup failed:', err); +});