feat: script per criptare frasi motivazionali/demotivazionali e spostate all'interno del sqllite. Mantenuti file txt per compatibilità. Aggiunta soglia di attivazione nel env
This commit is contained in:
+12
-1
@@ -23,4 +23,15 @@ DAILY_TARGET_TIME=480
|
|||||||
|
|
||||||
#Variabile per la numerazione dei ticket
|
#Variabile per la numerazione dei ticket
|
||||||
OTRS_SYSTEM_ID=10
|
OTRS_SYSTEM_ID=10
|
||||||
OTRS_COUNTER_PADDING=6
|
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
|
||||||
@@ -2395,4 +2395,30 @@ body {
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(-50%) translateX(0);
|
transform: translateY(-50%) translateX(0);
|
||||||
pointer-events: auto;
|
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;
|
||||||
}
|
}
|
||||||
+26
-11
@@ -387,26 +387,24 @@ const App = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/** Load demotivational phrases from txt file */
|
/** Load demotivational phrases from SQLite cache */
|
||||||
async loadDemotivationalPhrases() {
|
async loadDemotivationalPhrases() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/demotivational.txt');
|
const res = await fetch('/api/dashboard/phrases?tipo=demotivational');
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const text = await res.text();
|
this.demotivationalPhrases = await res.json();
|
||||||
this.demotivationalPhrases = text.split('\n').map(line => line.trim()).filter(line => line.length > 0);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Failed to load demotivational phrases:', e);
|
console.warn('Failed to load demotivational phrases:', e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/** Load motivational phrases from txt file */
|
/** Load motivational phrases from SQLite cache */
|
||||||
async loadMotivationalPhrases() {
|
async loadMotivationalPhrases() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/motivational.txt');
|
const res = await fetch('/api/dashboard/phrases?tipo=motivational');
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const text = await res.text();
|
this.motivationalPhrases = await res.json();
|
||||||
this.motivationalPhrases = text.split('\n').map(line => line.trim()).filter(line => line.length > 0);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Failed to load motivational phrases:', 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 data = await this.api('/api/users/time-today');
|
||||||
const todayTime = typeof data.totalToday === 'number' ? data.totalToday : 0;
|
const todayTime = typeof data.totalToday === 'number' ? data.totalToday : 0;
|
||||||
const remaining = Math.max(0, targetTime - todayTime);
|
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 = "";
|
let phrase = "";
|
||||||
const isDemotivational = percentage >= 70;
|
const phraseThreshold = this.lookups.config?.phraseThreshold || 70;
|
||||||
|
const isDemotivational = percentage >= phraseThreshold;
|
||||||
|
|
||||||
if (isDemotivational) {
|
if (isDemotivational) {
|
||||||
if (this.demotivationalPhrases.length > 0) {
|
if (this.demotivationalPhrases.length > 0) {
|
||||||
@@ -458,11 +468,16 @@ const App = {
|
|||||||
<div class="timer-tooltip-border"></div>
|
<div class="timer-tooltip-border"></div>
|
||||||
<div style="font-size:0.75rem; font-weight:600; color:var(--text-primary); margin-bottom:4px; display:flex; justify-content:space-between;">
|
<div style="font-size:0.75rem; font-weight:600; color:var(--text-primary); margin-bottom:4px; display:flex; justify-content:space-between;">
|
||||||
<span>Progresso Giornaliero</span>
|
<span>Progresso Giornaliero</span>
|
||||||
<strong>${percentage}%</strong>
|
<strong>${mathematicalPercentage}%</strong>
|
||||||
</div>
|
</div>
|
||||||
<div style="background:var(--border-light); border-radius:var(--radius-full); height:10px; width:100%; overflow:hidden; border:1px solid var(--border-subtle);">
|
<div style="background:var(--border-light); border-radius:var(--radius-full); height:10px; width:100%; overflow:hidden; border:1px solid var(--border-subtle);">
|
||||||
<div style="width:${percentage}%; background:linear-gradient(90deg, var(--accent-primary), var(--accent-secondary)); height:100%; border-radius:inherit; transition: width 0.3s ease;"></div>
|
<div style="width:${percentage}%; background:linear-gradient(90deg, var(--accent-primary), var(--accent-secondary)); height:100%; border-radius:inherit; transition: width 0.3s ease;"></div>
|
||||||
</div>
|
</div>
|
||||||
|
${hasOverperformance ? `
|
||||||
|
<div style="font-size:0.72rem; color:var(--error); font-weight:700; line-height:1.3; text-transform:uppercase; margin-top:8px; border-top:1px solid var(--border-subtle); padding-top:8px; text-align:center; animation: pulse 1.5s infinite;">
|
||||||
|
⚠️ Rilevata una overperformance allontanarsi dalla postazione immediatamente
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
<div style="font-size:0.72rem; color:var(--text-secondary); line-height:1.35; font-style:italic; margin-top:6px; border-top:1px solid var(--border-subtle); padding-top:6px; text-align:center;">
|
<div style="font-size:0.72rem; color:var(--text-secondary); line-height:1.35; font-style:italic; margin-top:6px; border-top:1px solid var(--border-subtle); padding-top:6px; text-align:center;">
|
||||||
"${phrase}"
|
"${phrase}"
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,75 @@
|
|||||||
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 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
|
// GET /api/dashboard/stats — Dashboard statistics
|
||||||
router.get('/stats', async (req, res) => {
|
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;
|
module.exports = router;
|
||||||
|
|||||||
+2
-1
@@ -135,7 +135,8 @@ router.get('/users', async (req, res) => {
|
|||||||
router.get('/config', (req, res) => {
|
router.get('/config', (req, res) => {
|
||||||
res.json({
|
res.json({
|
||||||
defaultAgentLogin: process.env.OTRS_API_USER || '',
|
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
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
Reference in New Issue
Block a user