feat: aggiunta tracciatura delle modifiche effettuate

This commit is contained in:
2026-07-07 21:51:58 +02:00
parent 3f6d2cc3a8
commit bf8b4c387e
10 changed files with 914 additions and 0 deletions
+3
View File
@@ -1,2 +1,5 @@
node_modules/ node_modules/
.env .env
internal.db
internal.db-shm
internal.db-wal
+64
View File
@@ -0,0 +1,64 @@
/**
* activityDb.js
* Local SQLite activity log database.
* Opens (or creates) internal.db and provides logAttivita() for logging
* every significant action sent to OTRS.
*/
const Database = require('better-sqlite3');
const path = require('path');
const crypto = require('crypto');
const DB_PATH = path.join(__dirname, 'internal.db');
const db = new Database(DB_PATH);
// Ensure WAL mode for better concurrent access
db.pragma('journal_mode = WAL');
// Create table if it does not exist
db.exec(`
CREATE TABLE IF NOT EXISTS attivita (
id TEXT PRIMARY KEY,
agente_id INTEGER NOT NULL DEFAULT 0,
agente_nome TEXT NOT NULL DEFAULT '',
titolo_azione TEXT NOT NULL,
azione TEXT NOT NULL DEFAULT '{}',
esito TEXT NOT NULL DEFAULT 'successo',
creato_il DATETIME NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
)
`);
/**
* Generate a UUID v7 (time-ordered).
*/
function uuidV7() {
const tsMs = BigInt(Date.now());
const tsMsHex = tsMs.toString(16).padStart(12, '0');
const rand = crypto.randomBytes(10).toString('hex');
const p1 = tsMsHex.slice(0, 8);
const p2 = tsMsHex.slice(8, 12);
const p3 = '7' + rand.slice(0, 3);
const p4 = ((parseInt(rand.slice(3, 4), 16) & 0x3) | 0x8).toString(16) + rand.slice(4, 7);
const p5 = rand.slice(7, 19);
return `${p1}-${p2}-${p3}-${p4}-${p5}`;
}
const insertStmt = db.prepare(`
INSERT INTO attivita (id, agente_id, agente_nome, titolo_azione, azione, esito)
VALUES (?, ?, ?, ?, ?, ?)
`);
/**
* Log an activity record.
* @param {Object} params
*/
function logAttivita({ agente_id = 0, agente_nome = '', titolo_azione, azione = {}, esito = 'successo' }) {
try {
const id = uuidV7();
const azioneStr = typeof azione === 'string' ? azione : JSON.stringify(azione, null, 2);
insertStmt.run(id, agente_id, agente_nome, titolo_azione, azioneStr, esito);
} catch (err) {
console.error('[activityDb] Failed to log activity:', err.message);
}
}
module.exports = { db, logAttivita };
+427
View File
@@ -9,6 +9,7 @@
"version": "1.0.0", "version": "1.0.0",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {
"better-sqlite3": "^12.11.1",
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"express": "^4.21.0", "express": "^4.21.0",
@@ -54,6 +55,60 @@
"node": ">= 6.0.0" "node": ">= 6.0.0"
} }
}, },
"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/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": { "node_modules/body-parser": {
"version": "1.20.5", "version": "1.20.5",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz",
@@ -78,6 +133,30 @@
"npm": "1.2.8000 || >= 1.4.16" "npm": "1.2.8000 || >= 1.4.16"
} }
}, },
"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": { "node_modules/bytes": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@@ -116,6 +195,12 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"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/content-disposition": { "node_modules/content-disposition": {
"version": "0.5.4", "version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@@ -178,6 +263,30 @@
"ms": "2.0.0" "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/denque": { "node_modules/denque": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
@@ -206,6 +315,15 @@
"npm": "1.2.8000 || >= 1.4.16" "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": { "node_modules/dotenv": {
"version": "16.6.1", "version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
@@ -247,6 +365,15 @@
"node": ">= 0.8" "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": { "node_modules/es-define-property": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@@ -292,6 +419,15 @@
"node": ">= 0.6" "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": { "node_modules/express": {
"version": "4.22.2", "version": "4.22.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz",
@@ -338,6 +474,12 @@
"url": "https://opencollective.com/express" "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/finalhandler": { "node_modules/finalhandler": {
"version": "1.3.2", "version": "1.3.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
@@ -374,6 +516,12 @@
"node": ">= 0.6" "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/function-bind": { "node_modules/function-bind": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -429,6 +577,12 @@
"node": ">= 0.4" "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/gopd": { "node_modules/gopd": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@@ -497,12 +651,38 @@
"node": ">=0.10.0" "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/inherits": { "node_modules/inherits": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC" "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": { "node_modules/ipaddr.js": {
"version": "1.9.1", "version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@@ -608,6 +788,33 @@
"node": ">= 0.6" "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/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": { "node_modules/ms": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@@ -664,6 +871,12 @@
"node": ">=8.0.0" "node": ">=8.0.0"
} }
}, },
"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": { "node_modules/negotiator": {
"version": "0.6.3", "version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
@@ -673,6 +886,18 @@
"node": ">= 0.6" "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/object-assign": { "node_modules/object-assign": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -706,6 +931,15 @@
"node": ">= 0.8" "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": { "node_modules/parseurl": {
"version": "1.3.3", "version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -849,6 +1083,33 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"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": { "node_modules/proxy-addr": {
"version": "2.0.7", "version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -862,6 +1123,16 @@
"node": ">= 0.10" "node": ">= 0.10"
} }
}, },
"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": { "node_modules/qs": {
"version": "6.15.3", "version": "6.15.3",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz",
@@ -902,6 +1173,35 @@
"node": ">= 0.8" "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/safe-buffer": { "node_modules/safe-buffer": {
"version": "5.2.1", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -928,6 +1228,18 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT" "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": { "node_modules/send": {
"version": "0.19.2", "version": "0.19.2",
"resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
@@ -1051,6 +1363,51 @@
"url": "https://github.com/sponsors/ljharb" "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/split2": { "node_modules/split2": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
@@ -1084,6 +1441,52 @@
"node": ">= 0.8" "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/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-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/toidentifier": { "node_modules/toidentifier": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
@@ -1093,6 +1496,18 @@
"node": ">=0.6" "node": ">=0.6"
} }
}, },
"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": { "node_modules/type-is": {
"version": "1.6.18", "version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
@@ -1122,6 +1537,12 @@
"node": ">= 0.8" "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": { "node_modules/utils-merge": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
@@ -1140,6 +1561,12 @@
"node": ">= 0.8" "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"
},
"node_modules/xtend": { "node_modules/xtend": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+1
View File
@@ -8,6 +8,7 @@
"dev": "npx -y nodemon server.js" "dev": "npx -y nodemon server.js"
}, },
"dependencies": { "dependencies": {
"better-sqlite3": "^12.11.1",
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"express": "^4.21.0", "express": "^4.21.0",
+10
View File
@@ -79,6 +79,15 @@
<span>Apertura Massiva</span> <span>Apertura Massiva</span>
</a> </a>
</li> </li>
<li>
<a href="#/activity" class="nav-link" data-view="activity" id="nav-activity">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10" />
<polyline points="12 6 12 12 16 14" />
</svg>
<span>Storico Attività</span>
</a>
</li>
</ul> </ul>
<div class="sidebar-footer"> <div class="sidebar-footer">
@@ -158,6 +167,7 @@
<script src="/js/views/ticketDetail.js"></script> <script src="/js/views/ticketDetail.js"></script>
<script src="/js/views/ticketCreate.js"></script> <script src="/js/views/ticketCreate.js"></script>
<script src="/js/views/ticketBulk.js"></script> <script src="/js/views/ticketBulk.js"></script>
<script src="/js/views/activityLog.js"></script>
<script src="/js/app.js"></script> <script src="/js/app.js"></script>
</body> </body>
+5
View File
@@ -138,6 +138,11 @@ const App = {
titleEl.textContent = 'Apertura Massiva Ticket'; titleEl.textContent = 'Apertura Massiva Ticket';
TicketBulkView.render(); TicketBulkView.render();
} else if (hash === '#/activity') {
document.getElementById('nav-activity')?.classList.add('active');
titleEl.textContent = 'Storico Attività';
ActivityLogView.render();
} else if (hash.match(/^#\/tickets\/(\d+)$/)) { } else if (hash.match(/^#\/tickets\/(\d+)$/)) {
const id = hash.match(/^#\/tickets\/(\d+)$/)[1]; const id = hash.match(/^#\/tickets\/(\d+)$/)[1];
document.getElementById('nav-tickets')?.classList.add('active'); document.getElementById('nav-tickets')?.classList.add('active');
+213
View File
@@ -0,0 +1,213 @@
/**
* ActivityLogView — Storico Attività
* Displays the local SQLite activity log with filters, dual pagination,
* and an expandable JSON detail panel.
*/
const ActivityLogView = {
currentPage: 1,
perPage: 50,
filters: { esito: '', agente_id: '', da: '', a: '' },
async render() {
this.currentPage = 1;
const container = document.getElementById('view-container');
container.innerHTML = '<div class="loading-screen"><div class="spinner"></div><p>Caricamento storico...</p></div>';
await this._draw();
},
async _draw() {
const container = document.getElementById('view-container');
try {
const params = new URLSearchParams({
page: this.currentPage,
per_page: this.perPage,
});
if (this.filters.esito) params.set('esito', this.filters.esito);
if (this.filters.agente_id) params.set('agente_id', this.filters.agente_id);
if (this.filters.da) params.set('da', this.filters.da);
if (this.filters.a) params.set('a', this.filters.a);
const data = await App.api(`/api/attivita?${params}`);
const { rows, total, page, per_page, total_pages } = data;
container.innerHTML = this._buildHtml(rows, total, page, per_page, total_pages);
this._bind();
} catch (err) {
container.innerHTML = `<div class="empty-state"><p style="color:var(--danger)">Errore caricamento: ${App.escapeHtml(err.message)}</p></div>`;
}
},
_buildHtml(rows, total, page, per_page, total_pages) {
const filtersHtml = this._buildFilters();
const paginationHtml = this._buildPagination(total, page, per_page, total_pages);
const tableHtml = rows.length === 0
? `<div class="empty-state" style="padding:var(--space-2xl);text-align:center;color:var(--text-muted);">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" style="width:48px;height:48px;margin:0 auto var(--space-md);display:block;opacity:0.4;"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
<p>Nessuna attività registrata.</p>
</div>`
: `<div class="table-wrapper" style="overflow-x:auto;">
<table class="tickets-table" id="activity-table" style="width:100%;">
<thead>
<tr>
<th style="width:160px;">Data/Ora</th>
<th style="width:160px;">Agente</th>
<th style="width:180px;">Azione</th>
<th style="min-width:60px;text-align:center;">Esito</th>
<th style="width:48px;text-align:center;">≡</th>
</tr>
</thead>
<tbody>
${rows.map(r => this._buildRow(r)).join('')}
</tbody>
</table>
</div>`;
return `
<div class="view-header" style="padding:var(--space-md) var(--space-xl);display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:var(--space-sm);border-bottom:1px solid var(--border-subtle);">
<div style="display:flex;align-items:center;gap:var(--space-sm);">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:20px;height:20px;color:var(--primary);"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
<span style="font-weight:600;font-size:1rem;">Storico Attività</span>
<span class="badge" style="background:var(--bg-tertiary);color:var(--text-secondary);font-size:0.75rem;padding:2px 8px;border-radius:12px;">${total} record</span>
</div>
</div>
${filtersHtml}
${paginationHtml}
${tableHtml}
${rows.length > 0 ? paginationHtml.replace(/id="pagination-top"/g,'id="pagination-bottom"') : ''}
`;
},
_buildRow(r) {
const dt = r.creato_il ? new Date(r.creato_il).toLocaleString('it-IT') : '—';
const esitoBadge = r.esito === 'successo'
? `<span style="display:inline-block;padding:2px 10px;border-radius:12px;background:rgba(34,197,94,0.15);color:#16a34a;font-size:0.75rem;font-weight:600;">✓ successo</span>`
: `<span style="display:inline-block;padding:2px 10px;border-radius:12px;background:rgba(239,68,68,0.15);color:#dc2626;font-size:0.75rem;font-weight:600;">✕ errore</span>`;
return `
<tr id="row-${r.id}" data-id="${r.id}">
<td style="font-size:0.8rem;color:var(--text-secondary);white-space:nowrap;">${dt}</td>
<td style="font-size:0.85rem;">${App.escapeHtml(r.agente_nome || '—')}</td>
<td style="font-size:0.85rem;font-weight:500;">${App.escapeHtml(r.titolo_azione)}</td>
<td style="text-align:center;">${esitoBadge}</td>
<td style="text-align:center;">
<button class="btn btn-ghost btn-sm act-detail-btn" data-id="${r.id}" title="Mostra dettaglio" style="padding:4px 8px;font-size:0.85rem;">≡</button>
</td>
</tr>
<tr id="detail-${r.id}" class="act-detail-row" style="display:none;">
<td colspan="5" style="padding:0 var(--space-md) var(--space-md);background:var(--bg-secondary);">
<pre style="margin:0;padding:var(--space-md);background:var(--bg-tertiary);border-radius:var(--radius-md);font-size:0.78rem;overflow-x:auto;white-space:pre-wrap;word-break:break-all;color:var(--text-primary);border:1px solid var(--border-subtle);">${App.escapeHtml(this._prettyJson(r.azione))}</pre>
</td>
</tr>
`;
},
_prettyJson(str) {
try {
return JSON.stringify(JSON.parse(str), null, 2);
} catch (_) {
return str || '';
}
},
_buildFilters() {
return `
<div id="activity-filters" style="display:flex;flex-wrap:wrap;gap:var(--space-sm);padding:var(--space-md) var(--space-xl);border-bottom:1px solid var(--border-subtle);background:var(--bg-secondary);align-items:flex-end;">
<div style="display:flex;flex-direction:column;gap:4px;">
<label style="font-size:0.75rem;color:var(--text-muted);font-weight:500;">Esito</label>
<select id="filter-esito" class="form-select" style="min-width:120px;height:36px;font-size:0.85rem;padding:6px 28px 6px 10px;">
<option value="">Tutti</option>
<option value="successo" ${this.filters.esito === 'successo' ? 'selected' : ''}>✓ Successo</option>
<option value="errore" ${this.filters.esito === 'errore' ? 'selected' : ''}>✕ Errore</option>
</select>
</div>
<div style="display:flex;flex-direction:column;gap:4px;">
<label style="font-size:0.75rem;color:var(--text-muted);font-weight:500;">Da data</label>
<input type="datetime-local" id="filter-da" class="form-input" value="${this.filters.da}" style="height:36px;font-size:0.85rem;padding:6px 10px;">
</div>
<div style="display:flex;flex-direction:column;gap:4px;">
<label style="font-size:0.75rem;color:var(--text-muted);font-weight:500;">A data</label>
<input type="datetime-local" id="filter-a" class="form-input" value="${this.filters.a}" style="height:36px;font-size:0.85rem;padding:6px 10px;">
</div>
<div style="display:flex;gap:var(--space-xs);align-self:flex-end;">
<button id="btn-apply-filters" class="btn btn-primary btn-sm" style="height:36px;">Applica</button>
<button id="btn-reset-filters" class="btn btn-ghost btn-sm" style="height:36px;">Reset</button>
</div>
</div>
`;
},
_buildPagination(total, page, per_page, total_pages) {
if (total_pages <= 1) return '';
const from = (page - 1) * per_page + 1;
const to = Math.min(page * per_page, total);
const pageBtn = (p, label, disabled, active) => {
const isDisabled = disabled || p === page;
return `<button class="btn btn-ghost btn-sm page-btn" data-page="${p}"
style="min-width:36px;height:32px;${active ? 'background:var(--primary);color:#fff;' : ''}${isDisabled ? 'opacity:0.4;pointer-events:none;' : ''}"
${disabled || active ? 'disabled' : ''}>${label}</button>`;
};
const pages = [];
pages.push(pageBtn(1, '«', page === 1, false));
pages.push(pageBtn(page - 1, '', page === 1, false));
const rangeStart = Math.max(1, page - 2);
const rangeEnd = Math.min(total_pages, page + 2);
for (let p = rangeStart; p <= rangeEnd; p++) {
pages.push(pageBtn(p, p, false, p === page));
}
pages.push(pageBtn(page + 1, '', page === total_pages, false));
pages.push(pageBtn(total_pages, '»', page === total_pages, false));
return `
<div id="pagination-top" style="display:flex;align-items:center;justify-content:space-between;padding:var(--space-sm) var(--space-xl);flex-wrap:wrap;gap:var(--space-sm);">
<span style="font-size:0.8rem;color:var(--text-muted);">Record ${from}${to} di ${total}</span>
<div style="display:flex;gap:4px;flex-wrap:wrap;">${pages.join('')}</div>
</div>
`;
},
_bind() {
// Filter apply
document.getElementById('btn-apply-filters')?.addEventListener('click', () => {
this.filters.esito = document.getElementById('filter-esito')?.value || '';
this.filters.da = document.getElementById('filter-da')?.value || '';
this.filters.a = document.getElementById('filter-a')?.value || '';
this.currentPage = 1;
this._draw();
});
// Filter reset
document.getElementById('btn-reset-filters')?.addEventListener('click', () => {
this.filters = { esito: '', agente_id: '', da: '', a: '' };
this.currentPage = 1;
this._draw();
});
// Detail expand/collapse
document.querySelectorAll('.act-detail-btn').forEach(btn => {
btn.addEventListener('click', () => {
const rowId = btn.dataset.id;
const detailRow = document.getElementById(`detail-${rowId}`);
if (!detailRow) return;
const isOpen = detailRow.style.display !== 'none';
detailRow.style.display = isOpen ? 'none' : 'table-row';
btn.textContent = isOpen ? '≡' : '✕';
});
});
// Pagination buttons (top and bottom share same .page-btn class)
document.querySelectorAll('.page-btn').forEach(btn => {
btn.addEventListener('click', () => {
const p = parseInt(btn.dataset.page, 10);
if (!isNaN(p)) {
this.currentPage = p;
this._draw();
document.getElementById('view-container')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
});
});
},
};
+66
View File
@@ -0,0 +1,66 @@
/**
* routes/activity.js
* GET /api/attivita — Paginates and filters the local SQLite activity log.
*/
const express = require('express');
const router = express.Router();
const { db } = require('../activityDb');
router.get('/', (req, res) => {
try {
const {
page = 1,
per_page = 50,
agente_id,
esito,
da,
a,
} = req.query;
const pageNum = Math.max(1, parseInt(page, 10));
const perPageNum = Math.min(200, Math.max(1, parseInt(per_page, 10)));
const offset = (pageNum - 1) * perPageNum;
const conditions = [];
const params = [];
if (agente_id) {
conditions.push('agente_id = ?');
params.push(parseInt(agente_id, 10));
}
if (esito) {
conditions.push('esito = ?');
params.push(esito);
}
if (da) {
conditions.push('creato_il >= ?');
params.push(da);
}
if (a) {
conditions.push('creato_il <= ?');
params.push(a);
}
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
const totalRow = db.prepare(`SELECT COUNT(*) AS cnt FROM attivita ${whereClause}`).get(...params);
const total = totalRow ? totalRow.cnt : 0;
const rows = db
.prepare(`SELECT * FROM attivita ${whereClause} ORDER BY creato_il DESC LIMIT ? OFFSET ?`)
.all(...params, perPageNum, offset);
res.json({
rows,
total,
page: pageNum,
per_page: perPageNum,
total_pages: Math.ceil(total / perPageNum),
});
} catch (err) {
console.error('[activity] Error fetching activities:', err);
res.status(500).json({ error: err.message });
}
});
module.exports = router;
+123
View File
@@ -1,6 +1,23 @@
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const pool = require('../db'); const pool = require('../db');
const { logAttivita } = require('../activityDb');
// Helper: resolve agent name from DB (best-effort, non-blocking)
async function resolveAgentName(agentId) {
try {
const r = await pool.query(
`SELECT first_name, last_name, login FROM users WHERE id = $1`,
[agentId]
);
if (r.rows.length > 0) {
const u = r.rows[0];
return (u.first_name || '') + ' ' + (u.last_name || '') || u.login || `Agent #${agentId}`;
}
} catch (_) { /* ignore */ }
return `Agent #${agentId}`;
}
// Helper for OTRS CE GenericInterface REST API calls // Helper for OTRS CE GenericInterface REST API calls
async function otrsRequest(method, path, bodyData = {}) { async function otrsRequest(method, path, bodyData = {}) {
@@ -301,6 +318,7 @@ router.get('/:id', async (req, res) => {
// POST /api/tickets — Create new ticket // POST /api/tickets — Create new ticket
// ============================================================ // ============================================================
router.post('/', async (req, res) => { router.post('/', async (req, res) => {
const operatorId = parseInt(req.headers['x-agent-id'], 10) || 1;
const client = await pool.connect(); const client = await pool.connect();
try { try {
const { const {
@@ -528,6 +546,17 @@ router.post('/', async (req, res) => {
await client.query('COMMIT'); await client.query('COMMIT');
// Log activity
resolveAgentName(operatorId).then(agente_nome => {
logAttivita({
agente_id: operatorId,
agente_nome,
titolo_azione: 'Creazione Ticket',
azione: { ticket_id: ticketId, tn: ticketResult.rows[0].tn, title, queue_id, state_id, priority_id, customer_id, customer_user_id },
esito: 'successo',
});
});
res.status(201).json({ res.status(201).json({
id: ticketId, id: ticketId,
tn: ticketResult.rows[0].tn, tn: ticketResult.rows[0].tn,
@@ -536,6 +565,9 @@ router.post('/', async (req, res) => {
} catch (err) { } catch (err) {
await client.query('ROLLBACK'); await client.query('ROLLBACK');
console.error('Error creating ticket:', err); console.error('Error creating ticket:', err);
resolveAgentName(operatorId).then(agente_nome => {
logAttivita({ agente_id: operatorId, agente_nome, titolo_azione: 'Creazione Ticket', azione: { error: err.message }, esito: 'errore' });
});
res.status(500).json({ error: err.message }); res.status(500).json({ error: err.message });
} finally { } finally {
client.release(); client.release();
@@ -620,6 +652,16 @@ router.patch('/:id', async (req, res) => {
}; };
} }
const result = await otrsRequest('PATCH', `/Ticket/${id}`, reqBody); const result = await otrsRequest('PATCH', `/Ticket/${id}`, reqBody);
const isClosing = updates.ticket_state_id && current.ticket_state_id !== updates.ticket_state_id;
resolveAgentName(operatorId).then(agente_nome => {
logAttivita({
agente_id: operatorId,
agente_nome,
titolo_azione: isClosing ? 'Chiusura Ticket' : 'Modifica Rapida Ticket',
azione: { ticket_id: id, ...updates },
esito: 'successo',
});
});
return res.json({ message: 'Ticket aggiornato! (via API REST)', result }); return res.json({ message: 'Ticket aggiornato! (via API REST)', result });
} }
return res.json({ message: 'Nessuna modifica rilevata' }); return res.json({ message: 'Nessuna modifica rilevata' });
@@ -819,10 +861,23 @@ router.patch('/:id', async (req, res) => {
} }
await client.query('COMMIT'); await client.query('COMMIT');
const isClosingDB = updates.ticket_state_id && current.ticket_state_id !== updates.ticket_state_id;
resolveAgentName(operatorId).then(agente_nome => {
logAttivita({
agente_id: operatorId,
agente_nome,
titolo_azione: isClosingDB ? 'Chiusura Ticket' : 'Modifica Rapida Ticket',
azione: { ticket_id: id, ...updates },
esito: 'successo',
});
});
res.json({ message: 'Ticket aggiornato! (via DB)' }); res.json({ message: 'Ticket aggiornato! (via DB)' });
} catch (err) { } catch (err) {
await client.query('ROLLBACK'); await client.query('ROLLBACK');
console.error('Error updating ticket:', err); console.error('Error updating ticket:', err);
resolveAgentName(operatorId).then(agente_nome => {
logAttivita({ agente_id: operatorId, agente_nome, titolo_azione: 'Modifica Rapida Ticket', azione: { ticket_id: id, error: err.message }, esito: 'errore' });
});
res.status(500).json({ error: err.message }); res.status(500).json({ error: err.message });
} finally { } finally {
client.release(); client.release();
@@ -902,6 +957,15 @@ router.post('/:id/articles', async (req, res) => {
} }
const result = await otrsRequest('PATCH', `/Ticket/${id}`, payload); const result = await otrsRequest('PATCH', `/Ticket/${id}`, payload);
resolveAgentName(operatorId).then(agente_nome => {
logAttivita({
agente_id: operatorId,
agente_nome,
titolo_azione: 'Aggiunta Nota',
azione: { ticket_id: id, subject, time_unit, has_attachments: !!(attachments && attachments.length) },
esito: 'successo',
});
});
return res.status(201).json({ return res.status(201).json({
message: 'Nota aggiunta! (via API REST)', message: 'Nota aggiunta! (via API REST)',
article_id: result.ArticleID, article_id: result.ArticleID,
@@ -1065,6 +1129,16 @@ router.post('/:id/articles', async (req, res) => {
await client.query('COMMIT'); await client.query('COMMIT');
resolveAgentName(operatorId).then(agente_nome => {
logAttivita({
agente_id: operatorId,
agente_nome,
titolo_azione: 'Aggiunta Nota',
azione: { ticket_id: id, subject, time_unit, has_attachments: !!(attachments && attachments.length) },
esito: 'successo',
});
});
res.status(201).json({ res.status(201).json({
article_id: articleId, article_id: articleId,
message: 'Nota aggiunta! (via DB)', message: 'Nota aggiunta! (via DB)',
@@ -1072,6 +1146,9 @@ router.post('/:id/articles', async (req, res) => {
} catch (err) { } catch (err) {
await client.query('ROLLBACK'); await client.query('ROLLBACK');
console.error('Error adding article:', err); console.error('Error adding article:', err);
resolveAgentName(operatorId).then(agente_nome => {
logAttivita({ agente_id: operatorId, agente_nome, titolo_azione: 'Aggiunta Nota', azione: { ticket_id: id, error: err.message }, esito: 'errore' });
});
res.status(500).json({ error: err.message }); res.status(500).json({ error: err.message });
} finally { } finally {
client.release(); client.release();
@@ -1123,6 +1200,15 @@ router.patch('/batch/update', async (req, res) => {
Ticket: ticketFields Ticket: ticketFields
}); });
} }
resolveAgentName(operatorId).then(agente_nome => {
logAttivita({
agente_id: operatorId,
agente_nome,
titolo_azione: 'Azione Massiva',
azione: { ticket_ids, updates },
esito: 'successo',
});
});
return res.json({ return res.json({
message: `${ticket_ids.length} ticket aggiornati! (via API REST)`, message: `${ticket_ids.length} ticket aggiornati! (via API REST)`,
updated_count: ticket_ids.length, updated_count: ticket_ids.length,
@@ -1172,6 +1258,16 @@ router.patch('/batch/update', async (req, res) => {
await client.query('COMMIT'); await client.query('COMMIT');
resolveAgentName(operatorId).then(agente_nome => {
logAttivita({
agente_id: operatorId,
agente_nome,
titolo_azione: 'Azione Massiva',
azione: { ticket_ids, updates },
esito: 'successo',
});
});
res.json({ res.json({
message: `${ticket_ids.length} ticket aggiornati! (via DB)`, message: `${ticket_ids.length} ticket aggiornati! (via DB)`,
updated_count: ticket_ids.length, updated_count: ticket_ids.length,
@@ -1179,6 +1275,9 @@ router.patch('/batch/update', async (req, res) => {
} catch (err) { } catch (err) {
await client.query('ROLLBACK'); await client.query('ROLLBACK');
console.error('Error batch updating tickets:', err); console.error('Error batch updating tickets:', err);
resolveAgentName(operatorId).then(agente_nome => {
logAttivita({ agente_id: operatorId, agente_nome, titolo_azione: 'Azione Massiva', azione: { ticket_ids, error: err.message }, esito: 'errore' });
});
res.status(500).json({ error: err.message }); res.status(500).json({ error: err.message });
} finally { } finally {
client.release(); client.release();
@@ -1382,10 +1481,22 @@ router.post('/merge', async (req, res) => {
} }
await client.query('COMMIT'); await client.query('COMMIT');
resolveAgentName(operatorId).then(agente_nome => {
logAttivita({
agente_id: operatorId,
agente_nome,
titolo_azione: 'Unione Ticket',
azione: { target_id: targetId, source_ids: sourceIds },
esito: 'successo',
});
});
res.json({ message: 'Ticket uniti con successo!' }); res.json({ message: 'Ticket uniti con successo!' });
} catch (err) { } catch (err) {
await client.query('ROLLBACK'); await client.query('ROLLBACK');
console.error('Error merging tickets:', err); console.error('Error merging tickets:', err);
resolveAgentName(operatorId).then(agente_nome => {
logAttivita({ agente_id: operatorId, agente_nome, titolo_azione: 'Unione Ticket', azione: { target_id: targetId, source_ids: sourceIds, error: err.message }, esito: 'errore' });
});
res.status(500).json({ error: err.message }); res.status(500).json({ error: err.message });
} finally { } finally {
client.release(); client.release();
@@ -1446,10 +1557,22 @@ router.put('/articles/:articleId/time', async (req, res) => {
} }
await client.query('COMMIT'); await client.query('COMMIT');
resolveAgentName(operatorId).then(agente_nome => {
logAttivita({
agente_id: operatorId,
agente_nome,
titolo_azione: 'Modifica Tempo Articolo',
azione: { article_id: articleId, time_unit: parsedTime },
esito: 'successo',
});
});
res.json({ message: 'Tempo aggiornato con successo!' }); res.json({ message: 'Tempo aggiornato con successo!' });
} catch (err) { } catch (err) {
await client.query('ROLLBACK'); await client.query('ROLLBACK');
console.error('Error updating article time:', err); console.error('Error updating article time:', err);
resolveAgentName(operatorId).then(agente_nome => {
logAttivita({ agente_id: operatorId, agente_nome, titolo_azione: 'Modifica Tempo Articolo', azione: { article_id: articleId, error: err.message }, esito: 'errore' });
});
res.status(500).json({ error: err.message }); res.status(500).json({ error: err.message });
} finally { } finally {
client.release(); client.release();
+2
View File
@@ -6,6 +6,7 @@ const path = require('path');
const ticketsRouter = require('./routes/tickets'); const ticketsRouter = require('./routes/tickets');
const lookupsRouter = require('./routes/lookups'); const lookupsRouter = require('./routes/lookups');
const dashboardRouter = require('./routes/dashboard'); const dashboardRouter = require('./routes/dashboard');
const activityRouter = require('./routes/activity');
const app = express(); const app = express();
const PORT = process.env.PORT || 3000; const PORT = process.env.PORT || 3000;
@@ -21,6 +22,7 @@ app.use(express.static(path.join(__dirname, 'public')));
app.use('/api/tickets', ticketsRouter); app.use('/api/tickets', ticketsRouter);
app.use('/api', lookupsRouter); app.use('/api', lookupsRouter);
app.use('/api/dashboard', dashboardRouter); app.use('/api/dashboard', dashboardRouter);
app.use('/api/attivita', activityRouter);
// SPA fallback — serve index.html for all non-API routes // SPA fallback — serve index.html for all non-API routes
app.get('*', (req, res) => { app.get('*', (req, res) => {