Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
60259427d3 | ||
|
|
dcc0456070 |
No files matched your search
@@ -182,13 +182,6 @@ if (dbType === 'mysql' || dbType === 'mariadb') {
|
|||||||
options: '-c timezone=UTC', // Ensure connection timezone is UTC
|
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) => {
|
pool.on('error', (err) => {
|
||||||
console.error('Unexpected error on idle client', err);
|
console.error('Unexpected error on idle client', err);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2779,3 +2779,262 @@ body {
|
|||||||
color: transparent !important;
|
color: transparent !important;
|
||||||
-webkit-text-fill-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;
|
||||||
|
}
|
||||||
@@ -279,6 +279,7 @@
|
|||||||
<script src="/js/views/envConfig.js"></script>
|
<script src="/js/views/envConfig.js"></script>
|
||||||
<script src="/js/views/userConfig.js"></script>
|
<script src="/js/views/userConfig.js"></script>
|
||||||
<script src="/js/views/noteTemporanee.js"></script>
|
<script src="/js/views/noteTemporanee.js"></script>
|
||||||
|
<script src="/js/views/consoleView.js"></script>
|
||||||
<script src="/js/app.js"></script>
|
<script src="/js/app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
+12
-3
@@ -68,6 +68,7 @@ const App = {
|
|||||||
pdf_save_edited: { ctrl: true, alt: false, shift: false, key: 'Enter', enabled: true },
|
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 },
|
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 },
|
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 };
|
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;
|
const isShift = sc.shift ? e.shiftKey : !e.shiftKey;
|
||||||
|
|
||||||
let targetKey = sc.key.toLowerCase();
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
return isCtrlOrCmd && isAlt && isShift && (pressedKey === targetKey);
|
const matchesKey = (pressedKey === targetKey) || (codeKey === targetKey);
|
||||||
|
return isCtrlOrCmd && isAlt && isShift && matchesKey;
|
||||||
},
|
},
|
||||||
|
|
||||||
formatShortcutLabel(sc) {
|
formatShortcutLabel(sc) {
|
||||||
@@ -302,6 +305,12 @@ const App = {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
window.location.hash = '#/tickets';
|
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);
|
}, true);
|
||||||
|
|
||||||
|
|||||||
@@ -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 = `
|
||||||
|
<div class="console-window">
|
||||||
|
<div class="console-header">
|
||||||
|
<div class="console-window-buttons">
|
||||||
|
<span class="console-btn btn-close" id="console-btn-close" title="Chiudi (Esc)"></span>
|
||||||
|
<span class="console-btn btn-minimize" id="console-btn-min"></span>
|
||||||
|
<span class="console-btn btn-maximize" id="console-btn-max"></span>
|
||||||
|
</div>
|
||||||
|
<div class="console-title">otrs-turbo@linux-server:~</div>
|
||||||
|
<div class="console-header-right">
|
||||||
|
<span class="console-badge">BASH v5.2.15</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="console-body" id="console-body">
|
||||||
|
<div class="console-welcome">
|
||||||
|
<pre class="console-banner">
|
||||||
|
____ _____ ____ ____ _____ _ _ ____ ____ ___
|
||||||
|
/ ___|_ _| _ \\/ ___| |_ _| | | | _ \\/ __ ) / _ \\
|
||||||
|
| | | | | |_) \\___ \\ | | | | | | |_) | _ \\| | | |
|
||||||
|
| |___ | | | _ < ___) | | | | |_| | _ <| |_) | |_| |
|
||||||
|
\\____| |_| |_| \\_\\____/ |_| \\___/|_| \\_\\____/ \\___/
|
||||||
|
</pre>
|
||||||
|
<div class="console-welcome-text">
|
||||||
|
Benvenuto nella console di simulazione Linux per <strong>OTRS Turbo</strong>.<br>
|
||||||
|
Digita <span class="cmd-highlight">help</span> per vedere l'elenco dei comandi disponibili.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="console-output"></div>
|
||||||
|
<div class="console-input-row" id="console-input-row">
|
||||||
|
<span class="console-prompt"><span class="user-part">agent@otrs-turbo</span>:<span class="path-part">~</span>$</span>
|
||||||
|
<input type="text" id="console-input" class="console-input" autocomplete="off" spellcheck="false" autocorrect="off" autocapitalize="none" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
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('<span class="text-warning">^C Animation interrupted</span>');
|
||||||
|
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(`<span class="text-muted">${matches.join(' ')}</span>`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
printPrompt(cmdText) {
|
||||||
|
const outputDiv = document.getElementById('console-output');
|
||||||
|
const line = document.createElement('div');
|
||||||
|
line.className = 'console-line';
|
||||||
|
line.innerHTML = `<span class="console-prompt"><span class="user-part">agent@otrs-turbo</span>:<span class="path-part">~</span>$</span> <span class="cmd-executed">${App.escapeHtml(cmdText)}</span>`;
|
||||||
|
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(`<span class="text-success">${App.escapeHtml(agent)}</span> (OTRS Turbo Operator)`);
|
||||||
|
break;
|
||||||
|
case 'sudo':
|
||||||
|
this.printOutput(`<span class="text-danger">agent is not in the sudoers file. This incident will be reported to OTRS Admins.</span>`);
|
||||||
|
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(`<span class="text-danger">bash: ${App.escapeHtml(mainCmd)}: command not found.</span> Digita <span class="cmd-highlight">help</span> per la lista dei comandi.`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
cmdHelp() {
|
||||||
|
const helpHtml = `
|
||||||
|
<div class="console-help-table">
|
||||||
|
<div><span class="cmd-name">help</span><span class="cmd-desc">Mostra questo messaggio di aiuto</span></div>
|
||||||
|
<div><span class="cmd-name">neofetch</span> / <span class="cmd-name">otrsfetch</span><span class="cmd-desc">Visualizza informazioni ed ASCII art di OTRS Turbo</span></div>
|
||||||
|
<div><span class="cmd-name">tickets</span><span class="cmd-desc">Elenca i ticket recenti ed i conteggi direttamente dal DB</span></div>
|
||||||
|
<div><span class="cmd-name">ls</span><span class="cmd-desc">Elenca i file della directory corrente</span></div>
|
||||||
|
<div><span class="cmd-name">cat <file></span><span class="cmd-desc">Legge il contenuto di un file virtuale</span></div>
|
||||||
|
<div><span class="cmd-name">whoami</span><span class="cmd-desc">Mostra l'agente attualmente autenticato</span></div>
|
||||||
|
<div><span class="cmd-name">theme <dark|green|amber|cyber></span><span class="cmd-desc">Cambia il tema cromatico del terminale</span></div>
|
||||||
|
<div><span class="cmd-name">matrix</span><span class="cmd-desc">Avvia l'animazione della pioggia digitale Matrix (Ctrl+C per uscire)</span></div>
|
||||||
|
<div><span class="cmd-name">sl</span><span class="cmd-desc">Fai passare la locomotiva a vapore ASCII nel terminale</span></div>
|
||||||
|
<div><span class="cmd-name">sudo <comando></span><span class="cmd-desc">Esegue un comando con privilegi di amministratore</span></div>
|
||||||
|
<div><span class="cmd-name">history</span><span class="cmd-desc">Mostra lo storico dei comandi usati</span></div>
|
||||||
|
<div><span class="cmd-name">clear</span><span class="cmd-desc">Pulisce lo schermo della console</span></div>
|
||||||
|
<div><span class="cmd-name">exit</span><span class="cmd-desc">Chiude la finestra del terminale</span></div>
|
||||||
|
</div>`;
|
||||||
|
this.printOutput(helpHtml);
|
||||||
|
},
|
||||||
|
|
||||||
|
cmdLs() {
|
||||||
|
this.printOutput(`
|
||||||
|
<span class="dir-item">drwxr-xr-x 2 agent otrs 4096 Sep 16 15:00 .</span>
|
||||||
|
<span class="dir-item">drwxr-xr-x 10 agent otrs 4096 Sep 16 15:00 ..</span>
|
||||||
|
<span class="file-item">-rw-r--r-- 1 agent otrs 2450 Sep 16 15:00 tickets.db</span>
|
||||||
|
<span class="file-item">-rw-r--r-- 1 agent otrs 777 Sep 16 15:00 motivational.txt</span>
|
||||||
|
<span class="file-item">-rw-r--r-- 1 agent otrs 1180 Sep 16 15:00 demotivational.txt</span>
|
||||||
|
<span class="file-item">-rw-r--r-- 1 agent otrs 512 Sep 16 15:00 secret.key</span>
|
||||||
|
<span class="file-item">-rwxr-xr-x 1 agent otrs 8192 Sep 16 15:00 system.log</span>
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
|
||||||
|
cmdCat(fileName) {
|
||||||
|
if (!fileName) {
|
||||||
|
this.printOutput('<span class="text-danger">cat: specifica un nome file. Es: cat motivational.txt</span>');
|
||||||
|
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(`<span class="text-info">💡 ${App.escapeHtml(random)}</span>`);
|
||||||
|
} 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(`<span class="text-warning">💀 ${App.escapeHtml(random)}</span>`);
|
||||||
|
} else if (name === 'secret.key') {
|
||||||
|
this.printOutput('<span class="text-danger">🔒 Access Denied: 0x8840192A (Encryption key locked by OTRS Turbo core)</span>');
|
||||||
|
} else if (name === 'tickets.db') {
|
||||||
|
this.printOutput('<span class="text-muted">SQLite/PG Binary format header. Usare il comando "tickets" per consultare i dati.</span>');
|
||||||
|
} else if (name === 'system.log') {
|
||||||
|
this.printOutput('<span class="text-muted">[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.</span>');
|
||||||
|
} else {
|
||||||
|
this.printOutput(`<span class="text-danger">cat: ${App.escapeHtml(fileName)}: No such file or directory</span>`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
cmdHistory() {
|
||||||
|
if (this.history.length === 0) {
|
||||||
|
this.printOutput('<span class="text-muted">Nessun comando nello storico.</span>');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const lines = this.history.map((c, i) => ` ${i + 1} ${App.escapeHtml(c)}`).join('\n');
|
||||||
|
this.printOutput(`<pre class="text-muted">${lines}</pre>`);
|
||||||
|
},
|
||||||
|
|
||||||
|
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 = `
|
||||||
|
<div class="neofetch-container">
|
||||||
|
<pre class="neofetch-logo">
|
||||||
|
██████╗ ████████╗██████╗ ██████╗
|
||||||
|
██╔═══██╗╚══██╔══╝██╔══██╗██╔════╝
|
||||||
|
██║ ██║ ██║ ██████╔╝╚█████╗
|
||||||
|
██║ ██║ ██║ ██╔══██╗ ╚═══██╗
|
||||||
|
╚██████╔╝ ██║ ██║ ██║██████╔╝
|
||||||
|
╚═════╝ ╚═╝ ╚═╝ ╚═╝╚═════╝
|
||||||
|
⚡ T U R B O E N G I N E
|
||||||
|
</pre>
|
||||||
|
<div class="neofetch-info">
|
||||||
|
<div><strong style="color:var(--accent-primary);">${App.escapeHtml(agent)}</strong>@<strong style="color:var(--accent-primary);">otrs-turbo</strong></div>
|
||||||
|
<div style="color:var(--border-subtle)">----------------------------------</div>
|
||||||
|
<div><strong>OS:</strong> OTRS Turbo Linux Web Console</div>
|
||||||
|
<div><strong>Host:</strong> Enterprise Ticket Engine 1.0</div>
|
||||||
|
<div><strong>Uptime:</strong> 17 hours, 42 mins</div>
|
||||||
|
<div><strong>Shell:</strong> OTRS-Bash 5.2</div>
|
||||||
|
<div><strong>Engine:</strong> Express / PG SQL Node.js 18.x</div>
|
||||||
|
<div><strong>Tickets In DB:</strong> ${ticketStats.total} total (${ticketStats.open} aperti)</div>
|
||||||
|
<div><strong>Theme:</strong> ${this.currentTheme}</div>
|
||||||
|
<div class="color-palette" style="margin-top:6px;">
|
||||||
|
<span style="background:#ef4444; display:inline-block; width:12px; height:12px; border-radius:2px; margin-right:3px;"></span>
|
||||||
|
<span style="background:#f59e0b; display:inline-block; width:12px; height:12px; border-radius:2px; margin-right:3px;"></span>
|
||||||
|
<span style="background:#10b981; display:inline-block; width:12px; height:12px; border-radius:2px; margin-right:3px;"></span>
|
||||||
|
<span style="background:#3b82f6; display:inline-block; width:12px; height:12px; border-radius:2px; margin-right:3px;"></span>
|
||||||
|
<span style="background:#8b5cf6; display:inline-block; width:12px; height:12px; border-radius:2px; margin-right:3px;"></span>
|
||||||
|
<span style="background:#ec4899; display:inline-block; width:12px; height:12px; border-radius:2px; margin-right:3px;"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
this.printOutput(html);
|
||||||
|
},
|
||||||
|
|
||||||
|
async cmdTickets() {
|
||||||
|
this.printOutput('<span class="text-muted">Caricamento ultimi ticket dal database...</span>');
|
||||||
|
try {
|
||||||
|
const res = await App.api('/api/tickets?page=1&per_page=8');
|
||||||
|
const tickets = res.tickets || [];
|
||||||
|
if (tickets.length === 0) {
|
||||||
|
this.printOutput('<span class="text-warning">Nessun ticket trovato.</span>');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let rows = `
|
||||||
|
<table class="console-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>TN</th>
|
||||||
|
<th>Titolo</th>
|
||||||
|
<th>Stato</th>
|
||||||
|
<th>Coda</th>
|
||||||
|
<th>Cliente</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
`;
|
||||||
|
|
||||||
|
tickets.forEach(t => {
|
||||||
|
rows += `
|
||||||
|
<tr>
|
||||||
|
<td class="text-accent">#${App.escapeHtml(t.tn)}</td>
|
||||||
|
<td>${App.escapeHtml((t.title || '').substring(0, 35))}</td>
|
||||||
|
<td><span class="badge-mini">${App.escapeHtml(t.state_name || '')}</span></td>
|
||||||
|
<td>${App.escapeHtml(t.queue_name || '')}</td>
|
||||||
|
<td>${App.escapeHtml(t.customer_first ? `${t.customer_first} ${t.customer_last}` : (t.customer_user_id || '—'))}</td>
|
||||||
|
</tr>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
rows += `</tbody></table>`;
|
||||||
|
this.printOutput(rows);
|
||||||
|
} catch (err) {
|
||||||
|
this.printOutput(`<span class="text-danger">Errore recupero ticket: ${App.escapeHtml(err.message)}</span>`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
cmdTheme(themeName) {
|
||||||
|
const valid = ['dark', 'green', 'amber', 'cyber'];
|
||||||
|
if (!themeName || !valid.includes(themeName.toLowerCase())) {
|
||||||
|
this.printOutput('<span class="text-warning">Uso: theme <dark|green|amber|cyber></span>');
|
||||||
|
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(`<span class="text-success">Tema terminale cambiato in: <strong>${t}</strong></span>`);
|
||||||
|
},
|
||||||
|
|
||||||
|
cmdMatrix() {
|
||||||
|
this.printOutput('<span class="text-success">Inizializzazione matrice in corso... premi Esc o Ctrl+C per fermare.</span>');
|
||||||
|
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('<span class="text-warning">Choo Choo! Locomotiva in transito...</span>');
|
||||||
|
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('<span class="text-success">La locomotiva ha completato la corsa!</span>');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const spaces = ' '.repeat(Math.max(0, pos));
|
||||||
|
trainBox.textContent = trainAscii.map(l => spaces + l).join('\n');
|
||||||
|
this.scrollToBottom();
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.ConsoleView = ConsoleView;
|
||||||
@@ -79,6 +79,13 @@ const ShortcutsView = {
|
|||||||
context: 'Globale',
|
context: 'Globale',
|
||||||
defaultCombo: { ctrl: true, alt: false, shift: true, key: 'Backspace', enabled: true }
|
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',
|
id: 'create_ticket_submit',
|
||||||
name: 'Invia Nuovo Ticket',
|
name: 'Invia Nuovo Ticket',
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ const TicketCreateView = {
|
|||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<div class="form-group full-width">
|
<div class="form-group full-width">
|
||||||
<label class="form-label">Titolo <span class="required">*</span></label>
|
<label class="form-label">Titolo <span class="required">*</span></label>
|
||||||
<input type="text" class="form-input" id="create-title" placeholder="Descrizione breve del problema" autofocus />
|
<input type="text" class="form-input" id="create-title" placeholder="Descrizione breve del problema" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" style="position:relative;">
|
<div class="form-group" style="position:relative;">
|
||||||
@@ -615,6 +615,11 @@ const TicketCreateView = {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Set focus on State dropdown (#create-state) on page load
|
||||||
|
setTimeout(() => {
|
||||||
|
document.getElementById('create-state')?.focus();
|
||||||
|
}, 50);
|
||||||
|
|
||||||
submitBtn.addEventListener('click', async () => {
|
submitBtn.addEventListener('click', async () => {
|
||||||
const title = document.getElementById('create-title').value.trim();
|
const title = document.getElementById('create-title').value.trim();
|
||||||
const queue_id = queueIdInput.value;
|
const queue_id = queueIdInput.value;
|
||||||
|
|||||||
+26
-1
@@ -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
|
// GET /api/tickets/:id — Single ticket detail
|
||||||
// ============================================================
|
// ============================================================
|
||||||
router.get('/:id', async (req, res) => {
|
router.get('/:id', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
if (isNaN(id)) {
|
||||||
|
return res.status(400).json({ error: 'ID ticket non valido' });
|
||||||
|
}
|
||||||
|
|
||||||
const ticketResult = await pool.query(
|
const ticketResult = await pool.query(
|
||||||
`SELECT
|
`SELECT
|
||||||
|
|||||||
Reference in new issue
Block a user