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:
@@ -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
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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