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);