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:
2026-07-08 03:09:44 +02:00
parent e6c3a6a43f
commit d39c9895b4
7 changed files with 208 additions and 13 deletions
+11
View File
@@ -24,3 +24,14 @@ DAILY_TARGET_TIME=480
#Variabile per la numerazione dei ticket
OTRS_SYSTEM_ID=10
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
+26
View File
@@ -2396,3 +2396,29 @@ body {
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;
}
+26 -11
View File
@@ -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 = {
<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;">
<span>Progresso Giornaliero</span>
<strong>${percentage}%</strong>
<strong>${mathematicalPercentage}%</strong>
</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="width:${percentage}%; background:linear-gradient(90deg, var(--accent-primary), var(--accent-secondary)); height:100%; border-radius:inherit; transition: width 0.3s ease;"></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;">
"${phrase}"
</div>
+90
View File
@@ -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;
+2 -1
View File
@@ -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
});
});
+30
View File
@@ -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);
}
+22
View File
@@ -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);