From 60259427d3cac9f87fb5c63b328ba489eeca62e2 Mon Sep 17 00:00:00 2001 From: Gabriele Cimaschi Date: Wed, 16 Sep 2026 15:55:57 +0200 Subject: [PATCH] feat: aggiunta console giocosa all'interno del sistema. fix: correzione di un warning. --- db.js | 7 - public/css/style.css | 259 +++++++++++++++++ public/index.html | 1 + public/js/app.js | 15 +- public/js/views/consoleView.js | 500 +++++++++++++++++++++++++++++++++ public/js/views/shortcuts.js | 7 + routes/tickets.js | 27 +- 7 files changed, 805 insertions(+), 11 deletions(-) create mode 100644 public/js/views/consoleView.js diff --git a/db.js b/db.js index f0d4588..988b26a 100644 --- a/db.js +++ b/db.js @@ -182,13 +182,6 @@ if (dbType === 'mysql' || dbType === 'mariadb') { options: '-c timezone=UTC', // Ensure connection timezone is UTC }); - // Ensure connection timezone is UTC via query fallback - pool.on('connect', (client) => { - client.query("SET TIME ZONE 'UTC'").catch(err => { - console.error('[DB] Errore nell\'impostazione della timezone UTC per Postgres:', err); - }); - }); - pool.on('error', (err) => { console.error('Unexpected error on idle client', err); }); diff --git a/public/css/style.css b/public/css/style.css index bddcaae..4ac3fde 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -2778,4 +2778,263 @@ body { background: rgba(59, 130, 246, 0.35) !important; color: transparent !important; -webkit-text-fill-color: transparent !important; +} + +/* ---- Linux Terminal Console ---- */ +.console-modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.75); + backdrop-filter: blur(6px); + z-index: 9999; + display: flex; + align-items: center; + justify-content: center; + padding: var(--space-md); + animation: fadeIn 0.2s ease; +} + +.console-window { + width: 900px; + max-width: 95vw; + height: 600px; + max-height: 85vh; + border-radius: 10px; + overflow: hidden; + box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.7); + display: flex; + flex-direction: column; + border: 1px solid rgba(255, 255, 255, 0.15); + font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', 'Courier New', monospace; + transition: all 0.2s ease; +} + +.console-header { + height: 38px; + background: #1e1e2e; + border-bottom: 1px solid rgba(255, 255, 255, 0.08); + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 16px; + user-select: none; +} + +.console-window-buttons { + display: flex; + align-items: center; + gap: 8px; +} + +.console-btn { + width: 12px; + height: 12px; + border-radius: 50%; + display: inline-block; + cursor: pointer; + transition: opacity 0.15s ease; +} + +.console-btn:hover { + opacity: 0.8; +} + +.console-btn.btn-close { background: #ff5f56; border: 1px solid #e0443e; } +.console-btn.btn-minimize { background: #ffbd2e; border: 1px solid #dea123; } +.console-btn.btn-maximize { background: #27c93f; border: 1px solid #1aab29; } + +.console-title { + font-size: 0.8rem; + color: #a6adc8; + font-weight: 600; +} + +.console-badge { + font-size: 0.7rem; + background: rgba(255, 255, 255, 0.1); + padding: 2px 8px; + border-radius: 4px; + color: #cdd6f4; +} + +.console-body { + flex: 1; + padding: 16px; + overflow-y: auto; + font-size: 0.88rem; + line-height: 1.45; + display: flex; + flex-direction: column; +} + +/* Themes */ +.theme-dark .console-window { background: #0f1419; color: #e6edf3; } +.theme-dark .console-prompt .user-part { color: #38bdf8; font-weight: bold; } +.theme-dark .console-prompt .path-part { color: #818cf8; } +.theme-dark .cmd-highlight { color: #38bdf8; font-weight: bold; } +.theme-dark .cmd-executed { color: #f43f5e; font-weight: bold; } + +.theme-green .console-window { background: #051a05; color: #4ade80; } +.theme-green .console-prompt .user-part { color: #22c55e; font-weight: bold; } +.theme-green .console-prompt .path-part { color: #86efac; } +.theme-green .cmd-highlight { color: #4ade80; font-weight: bold; } +.theme-green .cmd-executed { color: #a7f3d0; font-weight: bold; } +.theme-green .console-header { background: #082808; } + +.theme-amber .console-window { background: #1a1202; color: #fbbf24; } +.theme-amber .console-prompt .user-part { color: #f59e0b; font-weight: bold; } +.theme-amber .console-prompt .path-part { color: #fde68a; } +.theme-amber .cmd-highlight { color: #fbbf24; font-weight: bold; } +.theme-amber .cmd-executed { color: #fef08a; font-weight: bold; } +.theme-amber .console-header { background: #2a1d04; } + +.theme-cyber .console-window { background: #0d0221; color: #00f5d4; } +.theme-cyber .console-prompt .user-part { color: #7b2cbf; font-weight: bold; } +.theme-cyber .console-prompt .path-part { color: #f72585; } +.theme-cyber .cmd-highlight { color: #7209b7; font-weight: bold; } +.theme-cyber .cmd-executed { color: #4cc9f0; font-weight: bold; } +.theme-cyber .console-header { background: #19053d; } + +.console-banner { + font-size: 0.7rem; + line-height: 1.1; + color: var(--accent-primary); + margin-bottom: 12px; + font-weight: bold; +} + +.console-welcome-text { + margin-bottom: 16px; + color: #94a3b8; + font-size: 0.85rem; +} + +.console-line { + margin-bottom: 4px; +} + +.console-output-line { + margin-bottom: 8px; + word-break: break-word; + white-space: pre-wrap; +} + +.console-input-row { + display: flex; + align-items: center; + gap: 8px; + margin-top: 4px; +} + +.console-input { + flex: 1; + background: transparent; + border: none; + outline: none; + color: inherit; + font-family: inherit; + font-size: 0.88rem; + padding: 0; + caret-color: var(--accent-primary); +} + +.text-success { color: #22c55e; } +.text-danger { color: #ef4444; } +.text-warning { color: #f59e0b; } +.text-info { color: #3b82f6; } +.text-muted { color: #64748b; } +.text-accent { color: #38bdf8; font-weight: bold; } + +.dir-item { color: #60a5fa; font-weight: bold; } +.file-item { color: #e2e8f0; } + +.console-help-table { + display: flex; + flex-direction: column; + gap: 6px; + padding: 6px 0; +} + +.console-help-table > div { + display: flex; + gap: 16px; +} + +.cmd-name { + width: 220px; + flex-shrink: 0; + color: #38bdf8; + font-weight: bold; +} + +.cmd-desc { + color: #94a3b8; +} + +.neofetch-container { + display: flex; + gap: 24px; + align-items: flex-start; + padding: 8px 0; + flex-wrap: wrap; +} + +.neofetch-logo { + color: #38bdf8; + font-size: 0.75rem; + line-height: 1.15; + font-weight: bold; + margin: 0; +} + +.neofetch-info { + display: flex; + flex-direction: column; + gap: 3px; + font-size: 0.82rem; +} + +.console-table { + width: 100%; + border-collapse: collapse; + font-size: 0.82rem; + margin: 8px 0; +} + +.console-table th { + text-align: left; + border-bottom: 1px solid rgba(255, 255, 255, 0.2); + padding: 4px 8px; + color: #94a3b8; +} + +.console-table td { + padding: 4px 8px; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); +} + +.badge-mini { + background: rgba(255, 255, 255, 0.1); + padding: 1px 6px; + border-radius: 3px; + font-size: 0.75rem; +} + +.matrix-stream { + color: #22c55e; + font-size: 0.75rem; + line-height: 1.2; + margin: 0; + white-space: pre; +} + +.sl-train-box { + color: #f59e0b; + font-size: 0.8rem; + line-height: 1.2; + margin: 0; + white-space: pre; } \ No newline at end of file diff --git a/public/index.html b/public/index.html index a3833e6..557fe76 100644 --- a/public/index.html +++ b/public/index.html @@ -279,6 +279,7 @@ + diff --git a/public/js/app.js b/public/js/app.js index 240b650..d6f5e4b 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -68,6 +68,7 @@ const App = { pdf_save_edited: { ctrl: true, alt: false, shift: false, key: 'Enter', enabled: true }, ricerca_spudorata_open: { ctrl: true, alt: false, shift: true, key: 'f', enabled: true }, ticket_list_open: { ctrl: true, alt: false, shift: true, key: 'Backspace', enabled: true }, + open_terminal_console: { ctrl: true, alt: true, shift: false, key: 't', enabled: true }, }; return defaults[id] || { ctrl: true, alt: false, shift: false, key: 'Enter', enabled: true }; }, @@ -92,13 +93,15 @@ const App = { const isShift = sc.shift ? e.shiftKey : !e.shiftKey; let targetKey = sc.key.toLowerCase(); - let pressedKey = e.key.toLowerCase(); + let pressedKey = (e.key || '').toLowerCase(); + let codeKey = (e.code || '').replace(/^Key/, '').toLowerCase(); - if (targetKey === 'space' && (pressedKey === ' ' || pressedKey === 'space')) { + if (targetKey === 'space' && (pressedKey === ' ' || pressedKey === 'space' || codeKey === 'space')) { return isCtrlOrCmd && isAlt && isShift; } - return isCtrlOrCmd && isAlt && isShift && (pressedKey === targetKey); + const matchesKey = (pressedKey === targetKey) || (codeKey === targetKey); + return isCtrlOrCmd && isAlt && isShift && matchesKey; }, formatShortcutLabel(sc) { @@ -302,6 +305,12 @@ const App = { e.preventDefault(); window.location.hash = '#/tickets'; } + } else if (this.isShortcutTriggered(e, 'open_terminal_console')) { + e.preventDefault(); + const consoleObj = window.ConsoleView || (typeof ConsoleView !== 'undefined' ? ConsoleView : null); + if (consoleObj) { + consoleObj.toggle(); + } } }, true); diff --git a/public/js/views/consoleView.js b/public/js/views/consoleView.js new file mode 100644 index 0000000..e0a47b5 --- /dev/null +++ b/public/js/views/consoleView.js @@ -0,0 +1,500 @@ +/** + * ConsoleView — Linux Terminal Simulation for OTRS Turbo + * Shortcut: Ctrl + Alt + T + */ +const ConsoleView = { + isOpen: false, + history: [], + historyIndex: -1, + currentTheme: 'dark', // dark, green, amber, cyber + activeAnimation: null, + + toggle() { + if (this.isOpen) { + this.close(); + } else { + this.open(); + } + }, + + open() { + let modal = document.getElementById('linux-console-modal'); + if (!modal) { + this.createModal(); + modal = document.getElementById('linux-console-modal'); + } + modal.style.display = 'flex'; + this.isOpen = true; + const input = document.getElementById('console-input'); + if (input) { + input.focus(); + } + }, + + close() { + const modal = document.getElementById('linux-console-modal'); + if (modal) { + modal.style.display = 'none'; + } + this.stopAnimation(); + this.isOpen = false; + }, + + createModal() { + const overlay = document.createElement('div'); + overlay.id = 'linux-console-modal'; + overlay.className = 'console-modal-overlay theme-dark'; + overlay.innerHTML = ` +
+
+
+ + + +
+
otrs-turbo@linux-server:~
+
+ BASH v5.2.15 +
+
+
+
+
+  ____ _____ ____  ____    _____ _   _ ____  ____   ___  
+ / ___|_   _|  _ \\/ ___|  |_   _| | | |  _ \\/ __ ) / _ \\ 
+| |     | | | |_) \\___ \\    | | | | | | |_) |  _ \\| | | |
+| |___  | | |  _ < ___) |   | | | |_| |  _ <| |_) | |_| |
+ \\____| |_| |_| \\_\\____/    |_|  \\___/|_| \\_\\____/ \\___/ 
+            
+
+ Benvenuto nella console di simulazione Linux per OTRS Turbo.
+ Digita help per vedere l'elenco dei comandi disponibili. +
+
+
+
+ agent@otrs-turbo:~$ + +
+
+
+ `; + document.body.appendChild(overlay); + + // Event Bindings + overlay.querySelector('#console-btn-close').addEventListener('click', () => this.close()); + overlay.addEventListener('click', (e) => { + if (e.target === overlay) this.close(); + else { + const input = document.getElementById('console-input'); + if (input && document.activeElement !== input) input.focus(); + } + }); + + const input = overlay.querySelector('#console-input'); + input.addEventListener('keydown', (e) => this.handleKeyDown(e)); + }, + + stopAnimation() { + if (this.activeAnimation) { + clearInterval(this.activeAnimation); + this.activeAnimation = null; + } + }, + + handleKeyDown(e) { + if (e.key === 'Escape') { + e.preventDefault(); + this.close(); + return; + } + + if (e.key === 'Enter') { + const input = e.target; + const cmdText = input.value.trim(); + input.value = ''; + if (cmdText) { + this.history.push(cmdText); + this.historyIndex = this.history.length; + this.executeCommand(cmdText); + } else { + this.printPrompt(''); + } + return; + } + + if (e.key === 'ArrowUp') { + e.preventDefault(); + if (this.history.length > 0 && this.historyIndex > 0) { + this.historyIndex--; + e.target.value = this.history[this.historyIndex]; + } + return; + } + + if (e.key === 'ArrowDown') { + e.preventDefault(); + if (this.historyIndex < this.history.length - 1) { + this.historyIndex++; + e.target.value = this.history[this.historyIndex]; + } else { + this.historyIndex = this.history.length; + e.target.value = ''; + } + return; + } + + if (e.key === 'Tab') { + e.preventDefault(); + this.autocomplete(e.target); + return; + } + + if (e.ctrlKey && e.key.toLowerCase() === 'c') { + if (this.activeAnimation) { + e.preventDefault(); + this.stopAnimation(); + this.printOutput('^C Animation interrupted'); + this.showInputRow(); + } + } + }, + + autocomplete(inputEl) { + const val = inputEl.value.trim(); + const commands = ['help', 'neofetch', 'otrsfetch', 'tickets', 'ls', 'cat', 'matrix', 'sl', 'sudo', 'whoami', 'theme', 'clear', 'history', 'exit']; + const matches = commands.filter(c => c.startsWith(val)); + if (matches.length === 1) { + inputEl.value = matches[0] + ' '; + } else if (matches.length > 1) { + this.printOutput(`${matches.join(' ')}`); + } + }, + + printPrompt(cmdText) { + const outputDiv = document.getElementById('console-output'); + const line = document.createElement('div'); + line.className = 'console-line'; + line.innerHTML = `agent@otrs-turbo:~$ ${App.escapeHtml(cmdText)}`; + outputDiv.appendChild(line); + this.scrollToBottom(); + }, + + printOutput(htmlContent) { + const outputDiv = document.getElementById('console-output'); + const line = document.createElement('div'); + line.className = 'console-output-line'; + line.innerHTML = htmlContent; + outputDiv.appendChild(line); + this.scrollToBottom(); + }, + + scrollToBottom() { + const body = document.getElementById('console-body'); + if (body) { + body.scrollTop = body.scrollHeight; + } + }, + + showInputRow() { + const row = document.getElementById('console-input-row'); + if (row) row.style.display = 'flex'; + const input = document.getElementById('console-input'); + if (input) input.focus(); + }, + + hideInputRow() { + const row = document.getElementById('console-input-row'); + if (row) row.style.display = 'none'; + }, + + async executeCommand(rawCmd) { + this.stopAnimation(); + this.printPrompt(rawCmd); + const parts = rawCmd.trim().split(/\s+/); + const mainCmd = parts[0].toLowerCase(); + const args = parts.slice(1); + + switch (mainCmd) { + case 'help': + this.cmdHelp(); + break; + case 'clear': + document.getElementById('console-output').innerHTML = ''; + break; + case 'whoami': + const agent = App.activeAgent || 'gabriele.cimaschi'; + this.printOutput(`${App.escapeHtml(agent)} (OTRS Turbo Operator)`); + break; + case 'sudo': + this.printOutput(`agent is not in the sudoers file. This incident will be reported to OTRS Admins.`); + break; + case 'ls': + this.cmdLs(); + break; + case 'cat': + this.cmdCat(args[0]); + break; + case 'history': + this.cmdHistory(); + break; + case 'neofetch': + case 'otrsfetch': + await this.cmdNeofetch(); + break; + case 'tickets': + await this.cmdTickets(); + break; + case 'theme': + this.cmdTheme(args[0]); + break; + case 'matrix': + this.cmdMatrix(); + break; + case 'sl': + this.cmdSl(); + break; + case 'exit': + this.close(); + break; + default: + this.printOutput(`bash: ${App.escapeHtml(mainCmd)}: command not found. Digita help per la lista dei comandi.`); + break; + } + }, + + cmdHelp() { + const helpHtml = ` +
+
helpMostra questo messaggio di aiuto
+
neofetch / otrsfetchVisualizza informazioni ed ASCII art di OTRS Turbo
+
ticketsElenca i ticket recenti ed i conteggi direttamente dal DB
+
lsElenca i file della directory corrente
+
cat <file>Legge il contenuto di un file virtuale
+
whoamiMostra l'agente attualmente autenticato
+
theme <dark|green|amber|cyber>Cambia il tema cromatico del terminale
+
matrixAvvia l'animazione della pioggia digitale Matrix (Ctrl+C per uscire)
+
slFai passare la locomotiva a vapore ASCII nel terminale
+
sudo <comando>Esegue un comando con privilegi di amministratore
+
historyMostra lo storico dei comandi usati
+
clearPulisce lo schermo della console
+
exitChiude la finestra del terminale
+
`; + this.printOutput(helpHtml); + }, + + cmdLs() { + this.printOutput(` +drwxr-xr-x 2 agent otrs 4096 Sep 16 15:00 . +drwxr-xr-x 10 agent otrs 4096 Sep 16 15:00 .. +-rw-r--r-- 1 agent otrs 2450 Sep 16 15:00 tickets.db +-rw-r--r-- 1 agent otrs 777 Sep 16 15:00 motivational.txt +-rw-r--r-- 1 agent otrs 1180 Sep 16 15:00 demotivational.txt +-rw-r--r-- 1 agent otrs 512 Sep 16 15:00 secret.key +-rwxr-xr-x 1 agent otrs 8192 Sep 16 15:00 system.log + `); + }, + + cmdCat(fileName) { + if (!fileName) { + this.printOutput('cat: specifica un nome file. Es: cat motivational.txt'); + return; + } + const name = fileName.toLowerCase(); + if (name === 'motivational.txt') { + const phrases = App.motivationalPhrases || [ + "Ogni ticket chiuso è una vittoria verso la fine della giornata!", + "La velocità è la nostra virtù con OTRS Turbo!", + "Mantieni la calma ed applica le macro." + ]; + const random = phrases[Math.floor(Math.random() * phrases.length)]; + this.printOutput(`💡 ${App.escapeHtml(random)}`); + } else if (name === 'demotivational.txt') { + const phrases = App.demotivationalPhrases || [ + "Nessuna buona azione resta impunita: ogni ticket chiuso ne genera due nuovi.", + "Il weekend si avvicina, ma la coda di ticket non dorme mai.", + "Hai davvero controllato se il cavo di rete è collegato?" + ]; + const random = phrases[Math.floor(Math.random() * phrases.length)]; + this.printOutput(`💀 ${App.escapeHtml(random)}`); + } else if (name === 'secret.key') { + this.printOutput('🔒 Access Denied: 0x8840192A (Encryption key locked by OTRS Turbo core)'); + } else if (name === 'tickets.db') { + this.printOutput('SQLite/PG Binary format header. Usare il comando "tickets" per consultare i dati.'); + } else if (name === 'system.log') { + this.printOutput('[INFO] 2026-09-16 15:10:00 - OTRS Turbo Engine v1.0 running cleanly.\n[INFO] LDAP auto-sync skipped (within 24h window).\n[OK] Database query latency < 4ms.'); + } else { + this.printOutput(`cat: ${App.escapeHtml(fileName)}: No such file or directory`); + } + }, + + cmdHistory() { + if (this.history.length === 0) { + this.printOutput('Nessun comando nello storico.'); + return; + } + const lines = this.history.map((c, i) => ` ${i + 1} ${App.escapeHtml(c)}`).join('\n'); + this.printOutput(`
${lines}
`); + }, + + async cmdNeofetch() { + let ticketStats = { total: 0, open: 0 }; + try { + const data = await App.api('/api/tickets/stats'); + if (data) { + ticketStats.total = data.total_tickets || data.total || 0; + ticketStats.open = data.open_tickets || data.open || 0; + } + } catch (e) {} + + const agent = App.activeAgent || 'gabriele.cimaschi'; + const html = ` +
+ +
+
${App.escapeHtml(agent)}@otrs-turbo
+
----------------------------------
+
OS: OTRS Turbo Linux Web Console
+
Host: Enterprise Ticket Engine 1.0
+
Uptime: 17 hours, 42 mins
+
Shell: OTRS-Bash 5.2
+
Engine: Express / PG SQL Node.js 18.x
+
Tickets In DB: ${ticketStats.total} total (${ticketStats.open} aperti)
+
Theme: ${this.currentTheme}
+
+ + + + + + +
+
+
`; + this.printOutput(html); + }, + + async cmdTickets() { + this.printOutput('Caricamento ultimi ticket dal database...'); + try { + const res = await App.api('/api/tickets?page=1&per_page=8'); + const tickets = res.tickets || []; + if (tickets.length === 0) { + this.printOutput('Nessun ticket trovato.'); + return; + } + + let rows = ` + + + + + + + + + + + + `; + + tickets.forEach(t => { + rows += ` + + + + + + + `; + }); + + rows += `
TNTitoloStatoCodaCliente
#${App.escapeHtml(t.tn)}${App.escapeHtml((t.title || '').substring(0, 35))}${App.escapeHtml(t.state_name || '')}${App.escapeHtml(t.queue_name || '')}${App.escapeHtml(t.customer_first ? `${t.customer_first} ${t.customer_last}` : (t.customer_user_id || '—'))}
`; + this.printOutput(rows); + } catch (err) { + this.printOutput(`Errore recupero ticket: ${App.escapeHtml(err.message)}`); + } + }, + + cmdTheme(themeName) { + const valid = ['dark', 'green', 'amber', 'cyber']; + if (!themeName || !valid.includes(themeName.toLowerCase())) { + this.printOutput('Uso: theme <dark|green|amber|cyber>'); + return; + } + const t = themeName.toLowerCase(); + const overlay = document.getElementById('linux-console-modal'); + if (overlay) { + overlay.className = `console-modal-overlay theme-${t}`; + } + this.currentTheme = t; + this.printOutput(`Tema terminale cambiato in: ${t}`); + }, + + cmdMatrix() { + this.printOutput('Inizializzazione matrice in corso... premi Esc o Ctrl+C per fermare.'); + this.hideInputRow(); + const outputDiv = document.getElementById('console-output'); + const matrixBox = document.createElement('pre'); + matrixBox.className = 'matrix-stream'; + outputDiv.appendChild(matrixBox); + + const chars = '0123456789ABCDEFヲアウエオカキケコサシスセソタツテナニヌネハヒホマミムメモヤユラリワ'; + const cols = 60; + + this.activeAnimation = setInterval(() => { + let line = ''; + for (let i = 0; i < cols; i++) { + const char = chars[Math.floor(Math.random() * chars.length)]; + line += char + ' '; + } + matrixBox.textContent += line + '\n'; + this.scrollToBottom(); + if (matrixBox.textContent.length > 3000) { + matrixBox.textContent = matrixBox.textContent.substring(1500); + } + }, 80); + }, + + cmdSl() { + this.printOutput('Choo Choo! Locomotiva in transito...'); + this.hideInputRow(); + const outputDiv = document.getElementById('console-output'); + const trainBox = document.createElement('pre'); + trainBox.className = 'sl-train-box'; + outputDiv.appendChild(trainBox); + + const trainAscii = [ + " ==== ________ _____", + " _D _| |_______/ \\__I_I_____===__|__ |", + " | (_)--- ___/ OTRS \\_____/___======|___|", + " /___/____/______TURBO________/________(O)___/", + " $(O)(O)(O) (O)(O)(O)" + ]; + + let pos = 80; + this.activeAnimation = setInterval(() => { + pos -= 3; + if (pos < -60) { + this.stopAnimation(); + this.showInputRow(); + trainBox.remove(); + this.printOutput('La locomotiva ha completato la corsa!'); + return; + } + const spaces = ' '.repeat(Math.max(0, pos)); + trainBox.textContent = trainAscii.map(l => spaces + l).join('\n'); + this.scrollToBottom(); + }, 50); + } +}; + +window.ConsoleView = ConsoleView; diff --git a/public/js/views/shortcuts.js b/public/js/views/shortcuts.js index e19c8bb..9679bb3 100644 --- a/public/js/views/shortcuts.js +++ b/public/js/views/shortcuts.js @@ -79,6 +79,13 @@ const ShortcutsView = { context: 'Globale', defaultCombo: { ctrl: true, alt: false, shift: true, key: 'Backspace', enabled: true } }, + { + id: 'open_terminal_console', + name: 'Apri Terminale Linux', + description: 'Apre la finestra di simulazione del terminale Linux in overlay', + context: 'Globale', + defaultCombo: { ctrl: true, alt: true, shift: false, key: 't', enabled: true } + }, { id: 'create_ticket_submit', name: 'Invia Nuovo Ticket', diff --git a/routes/tickets.js b/routes/tickets.js index bca6b40..24745ad 100644 --- a/routes/tickets.js +++ b/routes/tickets.js @@ -313,12 +313,37 @@ router.get('/', async (req, res) => { } }); +// ============================================================ +// GET /api/tickets/stats — Ticket statistics overview +// ============================================================ +router.get('/stats', async (req, res) => { + try { + const totalRes = await pool.query('SELECT COUNT(*) as total FROM ticket'); + const openRes = await pool.query( + `SELECT COUNT(*) as total FROM ticket t + JOIN ticket_state ts ON t.ticket_state_id = ts.id + JOIN ticket_state_type tst ON ts.type_id = tst.id + WHERE tst.name IN ('new', 'open', 'pending auto', 'pending reminder')` + ); + res.json({ + total_tickets: parseInt(totalRes.rows[0]?.total || 0), + open_tickets: parseInt(openRes.rows[0]?.total || 0), + }); + } catch (err) { + console.error('Error fetching ticket stats:', err); + res.status(500).json({ error: err.message }); + } +}); + // ============================================================ // GET /api/tickets/:id — Single ticket detail // ============================================================ -router.get('/:id', async (req, res) => { +router.get('/:id', async (req, res, next) => { try { const { id } = req.params; + if (isNaN(id)) { + return res.status(400).json({ error: 'ID ticket non valido' }); + } const ticketResult = await pool.query( `SELECT