diff --git a/.env.example b/.env.example
index 481f866..c821e3b 100644
--- a/.env.example
+++ b/.env.example
@@ -23,4 +23,15 @@ DAILY_TARGET_TIME=480
#Variabile per la numerazione dei ticket
OTRS_SYSTEM_ID=10
-OTRS_COUNTER_PADDING=6
\ No newline at end of file
+OTRS_COUNTER_PADDING=6
+# Intervallo in ore per la sincronizzazione automatica LDAP (default 24 ore)
+LDAP_SYNC_INTERVAL_HOURS=24
+
+# Imposta a true per forzare l'aggiornamento diretto del DB per tutte le modifiche ai ticket bypassando l'API REST (esclusa la ricerca LDAP)
+FORCE_DB_UPDATE=false
+
+# Chiave per cifrare le frasi nel database locale (NON CANCELLARE O MODIFICARE SE CI SONO DATI CRIPTATI)
+CRYPTO_KEY=f30b91e92d77a06c59b20b2272e2cfbc
+
+# Soglia in percentuale del target tempo giornaliero per l'attivazione delle frasi demotivazionali (es. 70 per il 70%)
+PHRASE_THRESHOLD=70
\ No newline at end of file
diff --git a/public/css/style.css b/public/css/style.css
index 56ab5d1..d30ceb3 100644
--- a/public/css/style.css
+++ b/public/css/style.css
@@ -2395,4 +2395,30 @@ body {
opacity: 1;
transform: translateY(-50%) translateX(0);
pointer-events: auto;
+}
+
+/* ---- Overperformance Glow ---- */
+@keyframes overperformance-glow {
+ 0% {
+ box-shadow: 0 0 15px rgba(239, 68, 68, 0.4), inset 0 0 15px rgba(239, 68, 68, 0.2);
+ border-color: rgba(239, 68, 68, 0.6);
+ }
+ 50% {
+ box-shadow: 0 0 30px rgba(239, 68, 68, 0.8), inset 0 0 30px rgba(239, 68, 68, 0.4);
+ border-color: rgba(239, 68, 68, 1);
+ }
+ 100% {
+ box-shadow: 0 0 15px rgba(239, 68, 68, 0.4), inset 0 0 15px rgba(239, 68, 68, 0.2);
+ border-color: rgba(239, 68, 68, 0.6);
+ }
+}
+
+.sidebar-brand.glow {
+ animation: overperformance-glow 2s infinite;
+ background: rgba(239, 68, 68, 0.1) !important;
+ border: 1px solid rgba(239, 68, 68, 0.6) !important;
+ border-radius: var(--radius-md);
+ margin: var(--space-sm);
+ padding: 12px !important;
+ transition: all 0.5s ease;
}
\ No newline at end of file
diff --git a/public/js/app.js b/public/js/app.js
index 9e0cda8..9f3c037 100644
--- a/public/js/app.js
+++ b/public/js/app.js
@@ -387,26 +387,24 @@ const App = {
}
},
- /** Load demotivational phrases from txt file */
+ /** Load demotivational phrases from SQLite cache */
async loadDemotivationalPhrases() {
try {
- const res = await fetch('/demotivational.txt');
+ const res = await fetch('/api/dashboard/phrases?tipo=demotivational');
if (res.ok) {
- const text = await res.text();
- this.demotivationalPhrases = text.split('\n').map(line => line.trim()).filter(line => line.length > 0);
+ this.demotivationalPhrases = await res.json();
}
} catch (e) {
console.warn('Failed to load demotivational phrases:', e);
}
},
- /** Load motivational phrases from txt file */
+ /** Load motivational phrases from SQLite cache */
async loadMotivationalPhrases() {
try {
- const res = await fetch('/motivational.txt');
+ const res = await fetch('/api/dashboard/phrases?tipo=motivational');
if (res.ok) {
- const text = await res.text();
- this.motivationalPhrases = text.split('\n').map(line => line.trim()).filter(line => line.length > 0);
+ this.motivationalPhrases = await res.json();
}
} catch (e) {
console.warn('Failed to load motivational phrases:', e);
@@ -425,10 +423,22 @@ const App = {
const data = await this.api('/api/users/time-today');
const todayTime = typeof data.totalToday === 'number' ? data.totalToday : 0;
const remaining = Math.max(0, targetTime - todayTime);
- const percentage = Math.min(100, Math.round((todayTime / targetTime) * 100));
+ const mathematicalPercentage = Math.round((todayTime / targetTime) * 100);
+ const hasOverperformance = mathematicalPercentage > 100;
+ const percentage = Math.min(100, mathematicalPercentage);
+
+ const brandEl = document.querySelector('.sidebar-brand');
+ if (brandEl) {
+ if (hasOverperformance) {
+ brandEl.classList.add('glow');
+ } else {
+ brandEl.classList.remove('glow');
+ }
+ }
let phrase = "";
- const isDemotivational = percentage >= 70;
+ const phraseThreshold = this.lookups.config?.phraseThreshold || 70;
+ const isDemotivational = percentage >= phraseThreshold;
if (isDemotivational) {
if (this.demotivationalPhrases.length > 0) {
@@ -458,11 +468,16 @@ const App = {
Progresso Giornaliero
- ${percentage}%
+ ${mathematicalPercentage}%
+ ${hasOverperformance ? `
+
+ ⚠️ Rilevata una overperformance allontanarsi dalla postazione immediatamente
+
+ ` : ''}
"${phrase}"
diff --git a/routes/dashboard.js b/routes/dashboard.js
index 602b556..feaf8f7 100644
--- a/routes/dashboard.js
+++ b/routes/dashboard.js
@@ -1,6 +1,75 @@
const express = require('express');
const router = express.Router();
const pool = require('../db');
+const fs = require('fs');
+const path = require('path');
+const crypto = require('crypto');
+const { db } = require('../activityDb');
+
+const ALGORITHM = 'aes-256-cbc';
+const SECRET_KEY = crypto.createHash('sha256').update(process.env.CRYPTO_KEY || 'default_secret_key_12345').digest();
+const IV_LENGTH = 16;
+
+function encrypt(text) {
+ const iv = crypto.randomBytes(IV_LENGTH);
+ const cipher = crypto.createCipheriv(ALGORITHM, SECRET_KEY, iv);
+ let encrypted = cipher.update(text, 'utf8', 'hex');
+ encrypted += cipher.final('hex');
+ return iv.toString('hex') + ':' + encrypted;
+}
+
+function decrypt(text) {
+ const textParts = text.split(':');
+ const iv = Buffer.from(textParts.shift(), 'hex');
+ const encryptedText = Buffer.from(textParts.join(':'), 'hex');
+ const decipher = crypto.createDecipheriv(ALGORITHM, SECRET_KEY, iv);
+ let decrypted = decipher.update(encryptedText, 'hex', 'utf8');
+ decrypted += decipher.final('utf8');
+ return decrypted;
+}
+
+// Ensure SQLite table exists for phrases
+db.exec(`
+ CREATE TABLE IF NOT EXISTS frasi_cache (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ tipo TEXT,
+ testo TEXT
+ )
+`);
+
+// Seed function to import plain-text files into SQLite cache
+function seedPhrases() {
+ try {
+ const countRow = db.prepare("SELECT COUNT(*) AS count FROM frasi_cache").get();
+ if (countRow.count === 0) {
+ console.log('[Phrases Seed] SQLite frasi_cache is empty. Seeding...');
+
+ const seedFile = (fileName, type) => {
+ const txtPath = path.join(__dirname, `../public/${fileName}`);
+ if (fs.existsSync(txtPath)) {
+ const text = fs.readFileSync(txtPath, 'utf8');
+ const lines = text.split('\n').map(line => line.trim()).filter(line => line.length > 0);
+
+ const insertStmt = db.prepare("INSERT INTO frasi_cache (tipo, testo) VALUES (?, ?)");
+ db.transaction(() => {
+ for (const line of lines) {
+ insertStmt.run(type, encrypt(line));
+ }
+ })();
+ console.log(`[Phrases Seed] Successfully seeded ${lines.length} encrypted ${type} phrases.`);
+ }
+ };
+
+ seedFile('demotivational.txt', 'demotivational');
+ seedFile('motivational.txt', 'motivational');
+ }
+ } catch (err) {
+ console.error('[Phrases Seed] Seeding failed:', err.message);
+ }
+}
+
+seedPhrases();
+
// GET /api/dashboard/stats — Dashboard statistics
router.get('/stats', async (req, res) => {
@@ -116,4 +185,25 @@ router.get('/stats', async (req, res) => {
}
});
+// GET /api/dashboard/phrases — Get decrypted phrases from SQLite cache
+router.get('/phrases', (req, res) => {
+ const { tipo = 'demotivational' } = req.query;
+ try {
+ const rows = db.prepare("SELECT testo FROM frasi_cache WHERE tipo = ?").all(tipo);
+ const decryptedPhrases = rows.map(row => {
+ try {
+ return decrypt(row.testo);
+ } catch (decErr) {
+ console.warn('[Decrypt Phrase] Failed to decrypt phrase:', decErr.message);
+ return null;
+ }
+ }).filter(Boolean);
+
+ res.json(decryptedPhrases);
+ } catch (err) {
+ console.error('Error fetching decrypted phrases:', err);
+ res.status(500).json({ error: err.message });
+ }
+});
+
module.exports = router;
diff --git a/routes/lookups.js b/routes/lookups.js
index 694ed74..06b480e 100644
--- a/routes/lookups.js
+++ b/routes/lookups.js
@@ -135,7 +135,8 @@ router.get('/users', async (req, res) => {
router.get('/config', (req, res) => {
res.json({
defaultAgentLogin: process.env.OTRS_API_USER || '',
- dailyTargetTime: parseInt(process.env.DAILY_TARGET_TIME, 10) || 480
+ dailyTargetTime: parseInt(process.env.DAILY_TARGET_TIME, 10) || 480,
+ phraseThreshold: parseInt(process.env.PHRASE_THRESHOLD, 10) || 70
});
});
diff --git a/scripts/decrypt.js b/scripts/decrypt.js
new file mode 100644
index 0000000..7a10072
--- /dev/null
+++ b/scripts/decrypt.js
@@ -0,0 +1,30 @@
+const crypto = require('crypto');
+const path = require('path');
+require('dotenv').config({ path: path.join(__dirname, '../.env') });
+
+const ALGORITHM = 'aes-256-cbc';
+const SECRET_KEY = crypto.createHash('sha256').update(process.env.CRYPTO_KEY || 'default_secret_key_12345').digest();
+
+const encryptedText = process.argv[2];
+if (!encryptedText) {
+ console.log('Utilizzo: node decrypt.js "testo_criptato:valore"');
+ process.exit(1);
+}
+
+try {
+ const textParts = encryptedText.split(':');
+ if (textParts.length < 2) {
+ throw new Error('Formato testo cifrato non valido (manca il separatore ":")');
+ }
+ const iv = Buffer.from(textParts.shift(), 'hex');
+ const encrypted = Buffer.from(textParts.join(':'), 'hex');
+ const decipher = crypto.createDecipheriv(ALGORITHM, SECRET_KEY, iv);
+ let decrypted = decipher.update(encrypted, 'hex', 'utf8');
+ decrypted += decipher.final('utf8');
+
+ console.log('Testo Criptato: ', encryptedText);
+ console.log('Testo Decriptato:', decrypted);
+} catch (err) {
+ console.error('Errore durante la decriptazione:', err.message);
+ process.exit(1);
+}
diff --git a/scripts/encrypt.js b/scripts/encrypt.js
new file mode 100644
index 0000000..0ef0cf2
--- /dev/null
+++ b/scripts/encrypt.js
@@ -0,0 +1,22 @@
+const crypto = require('crypto');
+const path = require('path');
+require('dotenv').config({ path: path.join(__dirname, '../.env') });
+
+const ALGORITHM = 'aes-256-cbc';
+const SECRET_KEY = crypto.createHash('sha256').update(process.env.CRYPTO_KEY || 'default_secret_key_12345').digest();
+const IV_LENGTH = 16;
+
+const text = process.argv[2];
+if (!text) {
+ console.log('Utilizzo: node encrypt.js "testo da criptare"');
+ process.exit(1);
+}
+
+const iv = crypto.randomBytes(IV_LENGTH);
+const cipher = crypto.createCipheriv(ALGORITHM, SECRET_KEY, iv);
+let encrypted = cipher.update(text, 'utf8', 'hex');
+encrypted += cipher.final('hex');
+const result = iv.toString('hex') + ':' + encrypted;
+
+console.log('Testo Originale:', text);
+console.log('Testo Criptato: ', result);