prevedi un postgres sql
ci/woodpecker/push/ci Pipeline failed
ci/woodpecker/tag/ci Pipeline failed
ci/woodpecker/tag/deploy-test Pipeline was successful

This commit is contained in:
Yan Xell
2026-05-19 15:34:27 +02:00
parent cec6f3694b
commit 956f8aec56
11 changed files with 421 additions and 42 deletions
+3
View File
@@ -11,3 +11,6 @@ __pycache__/
*.db-journal
*.db-wal
*.db-shm
# Daily log files
logs/
+16
View File
@@ -22,6 +22,11 @@ class DbType(StrEnum):
UNKNOWN = 'unknown'
class ConnType(StrEnum):
SQLITE = 'sqlite'
POSTGRES = 'postgres'
_db_dir_env = os.environ.get('RPA_DB_DIR', '')
DB_DEFAULT_DIR = Path(_db_dir_env) if _db_dir_env else REPORTS_DIR.parent / "data_rpa" / "db"
@@ -45,3 +50,14 @@ _auth: dict = {
'password': os.environ.get('DASHBOARD_PASSWORD', ''),
}
_sessions: set = set() # active session tokens
# Database connection — mutable so settings can be updated at runtime
_conn_type_raw = os.environ.get('RPA_CONN_TYPE', 'sqlite').strip().lower()
_conn: dict = {
'type': ConnType.POSTGRES if _conn_type_raw == 'postgres' else ConnType.SQLITE,
'pg_host': os.environ.get('PG_HOST', 'localhost'),
'pg_port': os.environ.get('PG_PORT', '5432'),
'pg_db': os.environ.get('PG_DB', ''),
'pg_user': os.environ.get('PG_USER', ''),
'pg_password': os.environ.get('PG_PASSWORD', ''),
}
+30 -1
View File
@@ -4,7 +4,7 @@
import re
from .config import REPORTS_DIR, _auth
from .config import REPORTS_DIR, _auth, _conn, ConnType
def _set_env_key(text: str, key: str, value: str) -> str:
@@ -31,3 +31,32 @@ def update_auth(enabled: bool, user: str | None = None, password: str | None = N
_auth['user'] = user
if password is not None:
_auth['password'] = password
def update_db_conn(
conn_type: str,
pg_host: str = '',
pg_port: str = '5432',
pg_db: str = '',
pg_user: str = '',
pg_password: str = '',
) -> None:
"""Write RPA_CONN_TYPE and PG_* vars to .env and update _conn in memory."""
env_path = REPORTS_DIR / '.env'
text = env_path.read_text(encoding='utf-8')
text = _set_env_key(text, 'RPA_CONN_TYPE', conn_type)
text = _set_env_key(text, 'PG_HOST', pg_host or 'localhost')
text = _set_env_key(text, 'PG_PORT', pg_port or '5432')
text = _set_env_key(text, 'PG_DB', pg_db)
text = _set_env_key(text, 'PG_USER', pg_user)
text = _set_env_key(text, 'PG_PASSWORD', pg_password)
env_path.write_text(text, encoding='utf-8')
_conn['type'] = ConnType.POSTGRES if conn_type == 'postgres' else ConnType.SQLITE
_conn['pg_host'] = pg_host or 'localhost'
_conn['pg_port'] = pg_port or '5432'
_conn['pg_db'] = pg_db
_conn['pg_user'] = pg_user
_conn['pg_password'] = pg_password
+47
View File
@@ -0,0 +1,47 @@
import logging
from logging.handlers import TimedRotatingFileHandler
from pathlib import Path
_logger: logging.Logger | None = None
def get_logger() -> logging.Logger:
global _logger
if _logger is None:
_logger = logging.getLogger('rpa_dashboard')
return _logger
def setup_logging(log_dir: Path, backup_count: int = 30) -> logging.Logger:
global _logger
log_dir.mkdir(parents=True, exist_ok=True)
logger = logging.getLogger('rpa_dashboard')
logger.setLevel(logging.INFO)
if logger.handlers:
return logger
fmt = logging.Formatter(
'[%(asctime)s] %(levelname)-5s %(message)s',
datefmt='%Y-%m-%d %H:%M:%S',
)
# Daily rotation at midnight — keeps last `backup_count` days
# Current day: logs/dashboard.log
# Past days: logs/dashboard.log.YYYY-MM-DD
file_handler = TimedRotatingFileHandler(
log_dir / 'dashboard.log',
when='midnight',
backupCount=backup_count,
encoding='utf-8',
)
file_handler.setFormatter(fmt)
logger.addHandler(file_handler)
console = logging.StreamHandler()
console.setFormatter(fmt)
logger.addHandler(console)
_logger = logger
return logger
+5 -5
View File
@@ -5,11 +5,6 @@ from datetime import date as _date
from .config import _state, _access_log, DbType, LOGS_PER_PAGE, STEPS_ROWS
from .renderer import _base, _card, _page_css, _page_js, _tmpl, _css
from .queries.db import e as _e
_IT_MONTHS = [
'', 'Gennaio', 'Febbraio', 'Marzo', 'Aprile', 'Maggio', 'Giugno',
'Luglio', 'Agosto', 'Settembre', 'Ottobre', 'Novembre', 'Dicembre'
]
from .queries import processes as q_processes
from .queries import pec as q_pec
from .queries import logs as q_logs
@@ -20,6 +15,11 @@ from .queries import api_iscrizioni as q_api_iscrizioni
from .queries import email as q_email
from .queries import sharepoint as q_sharepoint
_IT_MONTHS = [
'', 'Gennaio', 'Febbraio', 'Marzo', 'Aprile', 'Maggio', 'Giugno',
'Luglio', 'Agosto', 'Settembre', 'Ottobre', 'Novembre', 'Dicembre'
]
def _load_data(db_path: str) -> dict:
db_type = _state.get('db_type', 'unknown')
+36 -4
View File
@@ -1,4 +1,4 @@
"""queries/db.py — helper condiviso per query SQLite"""
"""queries/db.py — helper condiviso per query SQLite / PostgreSQL"""
import sqlite3
from datetime import datetime
@@ -8,7 +8,33 @@ if TYPE_CHECKING:
from ..config import DbType
def _pg_query(sql: str, params: tuple = ()) -> list:
"""Execute a query against PostgreSQL using connection params from _conn."""
import psycopg2
import psycopg2.extras
from ..config import _conn
conn = psycopg2.connect(
host=_conn['pg_host'],
port=int(_conn['pg_port'] or 5432),
dbname=_conn['pg_db'],
user=_conn['pg_user'],
password=_conn['pg_password'],
connect_timeout=10,
)
try:
cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
# SQLite uses ? placeholders; psycopg2 uses %s
pg_sql = sql.replace('?', '%s')
cur.execute(pg_sql, params)
return [dict(r) for r in cur.fetchall()]
finally:
conn.close()
def query(db_path: str, sql: str, params: tuple = ()) -> list:
from ..config import _conn, ConnType
if _conn['type'] == ConnType.POSTGRES:
return _pg_query(sql, params)
conn = sqlite3.connect(db_path)
conn.row_factory = sqlite3.Row
try:
@@ -70,13 +96,19 @@ def badge(cls: str, text: str) -> str:
def detect_db(db_path: str) -> 'DbType':
"""Returns DB_TYPE_REG_LOMB, DB_TYPE_INTRAZ, or DB_TYPE_UNKNOWN based on DB schema.
"""Returns REG_LOMB, INTRAZ, or UNKNOWN based on DB schema.
reg_Lomb (db_reg_lombardia) → has table 'sessione_documenti'
Intraz (db_corsi_intraziendali) → has table 'rpa_intra_api_iscrizione'
"""
from ..config import DbType
rows = query(db_path, "SELECT name FROM sqlite_master WHERE type='table'")
from ..config import DbType, _conn, ConnType
if _conn['type'] == ConnType.POSTGRES:
rows = _pg_query(
"SELECT table_name AS name FROM information_schema.tables "
"WHERE table_schema='public'"
)
else:
rows = query(db_path, "SELECT name FROM sqlite_master WHERE type='table'")
tables = {r['name'] for r in rows}
if 'sessione_documenti' in tables:
return DbType.REG_LOMB
+83 -2
View File
@@ -2,7 +2,7 @@ from datetime import datetime
from pathlib import Path
from string import Template
from .config import TMPL_DIR, CSS_PATH, PAGES_CSS_DIR, JS_DIR, _state, DbType, _auth
from .config import TMPL_DIR, CSS_PATH, PAGES_CSS_DIR, JS_DIR, _state, DbType, _auth, _conn, ConnType
from .queries.db import e as _e
@@ -57,6 +57,81 @@ def _card(label, value, color='#2563eb') -> str:
)
def _build_db_conn_modal() -> str:
is_pg = _conn['type'] == ConnType.POSTGRES
inp = (
'style="width:100%;padding:.5rem .75rem;border:1.5px solid #cbd5e1;'
'border-radius:6px;font-size:.9rem;box-sizing:border-box"'
)
lbl = 'style="font-size:.8rem;font-weight:600;color:#475569;display:block;margin-bottom:.25rem"'
badge_sqlite = (
'<span class="badge ok" style="font-size:.85rem;padding:4px 12px">&#128196; SQLite</span>'
)
badge_pg = (
'<span class="badge" style="font-size:.85rem;padding:4px 12px;background:#7c3aed;color:#fff">'
'&#128225; PostgreSQL</span>'
)
current_badge = badge_pg if is_pg else badge_sqlite
pg_host = _e(_conn.get('pg_host', 'localhost'))
pg_port = _e(_conn.get('pg_port', '5432'))
pg_db = _e(_conn.get('pg_db', ''))
pg_user = _e(_conn.get('pg_user', ''))
body = (
'<p class="modal-subtitle">Sorgente dati attuale:</p>'
f'<p style="margin-bottom:1rem">{current_badge}</p>'
'<form method="POST" action="/settings/db" style="display:flex;flex-direction:column;gap:.75rem">'
'<div style="display:flex;gap:.5rem;margin-bottom:.25rem">'
f'<label style="flex:1;display:flex;align-items:center;gap:.4rem;cursor:pointer;'
f'padding:.5rem .75rem;border:1.5px solid {"#7c3aed" if not is_pg else "#cbd5e1"};'
f'border-radius:6px;font-size:.9rem">'
f'<input type="radio" name="conn_type" value="sqlite" {"checked" if not is_pg else ""}>'
f'&nbsp;SQLite (file locale)</label>'
f'<label style="flex:1;display:flex;align-items:center;gap:.4rem;cursor:pointer;'
f'padding:.5rem .75rem;border:1.5px solid {"#7c3aed" if is_pg else "#cbd5e1"};'
f'border-radius:6px;font-size:.9rem">'
f'<input type="radio" name="conn_type" value="postgres" {"checked" if is_pg else ""}>'
f'&nbsp;PostgreSQL (remoto)</label>'
'</div>'
'<fieldset id="pg-fields" style="border:1.5px solid #e2e8f0;border-radius:6px;padding:.75rem;'
f'display:{"block" if is_pg else "none"}">'
'<legend style="font-size:.8rem;font-weight:600;color:#7c3aed;padding:0 .4rem">Connessione PostgreSQL</legend>'
f'<div style="display:grid;grid-template-columns:1fr auto;gap:.5rem;margin-bottom:.5rem">'
f'<div><label {lbl}>Host</label><input type="text" name="pg_host" value="{pg_host}" {inp}></div>'
f'<div style="width:80px"><label {lbl}>Porta</label><input type="text" name="pg_port" value="{pg_port}" {inp}></div>'
'</div>'
f'<div style="margin-bottom:.5rem"><label {lbl}>Database</label><input type="text" name="pg_db" value="{pg_db}" {inp}></div>'
f'<div style="margin-bottom:.5rem"><label {lbl}>Utente</label><input type="text" name="pg_user" value="{pg_user}" {inp}></div>'
f'<div><label {lbl}>Password</label><input type="password" name="pg_password" placeholder="(invariata se vuota)" {inp}></div>'
'</fieldset>'
'<button type="submit" style="width:100%;padding:.55rem;background:#2563eb;color:#fff;border:none;'
'border-radius:6px;font-size:.9rem;font-weight:600;cursor:pointer">'
'&#128190; Salva e Riconnetti'
'</button>'
'</form>'
'<script>'
'(function(){'
'var radios=document.querySelectorAll(\'[name="conn_type"]\');'
'var fields=document.getElementById(\'pg-fields\');'
'radios.forEach(function(r){'
'r.addEventListener(\'change\',function(){'
'fields.style.display=this.value==="postgres"?"block":"none";'
'});});})();'
'</script>'
)
return (
'<div id="modal-db-conn" class="modal-overlay">'
'<div class="modal-box">'
'<button class="modal-close"'
' onclick="document.getElementById(\'modal-db-conn\').classList.remove(\'open\')"'
' title="Chiudi">&#10005;</button>'
'<h3>&#128225; Sorgente Dati</h3>'
+ body +
'</div></div>'
)
def _build_login_modal() -> str:
enabled = _auth['enabled']
inp = (
@@ -141,9 +216,14 @@ def _base(title: str, content: str, active: str, db_path: str, page_css: str = '
nav_report_link = f'<a href="/report" class="{cls_rep}">Report</a>'
nav_sharepoint_link = ''
if _conn['type'] == ConnType.POSTGRES:
db_display = _e(f"{_conn['pg_host']}:{_conn['pg_port']}/{_conn['pg_db']}")
else:
db_display = _e(Path(db_path).name) if db_path else '-'
return _tmpl('_base.html').substitute(
css=_css(), page_css=page_css, page_js=page_js, title=title, now=now,
db_name=_e(Path(db_path).name),
db_name=db_display,
h1_title=h1_title,
content=content,
nav_step_link=nav_step_link,
@@ -151,6 +231,7 @@ def _base(title: str, content: str, active: str, db_path: str, page_css: str = '
nav_sharepoint_link=nav_sharepoint_link,
nav_pec_link=nav_pec_link,
modal_login=_build_login_modal(),
modal_db_conn=_build_db_conn_modal(),
logout_btn='<a href="/logout" class="logout-btn" title="Esci">&#128274; Esci</a>' if _auth['enabled'] else '',
**nav,
)
+88 -30
View File
@@ -6,7 +6,9 @@ from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import urlparse, parse_qs, unquote_plus
from .config import _state, _access_log, DbType, _auth, _sessions, LOGS_PER_PAGE, STEPS_ROWS
from .env_manager import update_auth as _update_env_auth
from .config import _conn, ConnType, REPORTS_DIR
from .logger import get_logger
from .env_manager import update_auth as _update_env_auth, update_db_conn as _update_db_conn
from .db_finder import _find_db, _pick_db_file
from .renderer import _base as _render_base
from .pages import (
@@ -51,7 +53,7 @@ def make_handler(db_path: str):
class ReportHandler(BaseHTTPRequestHandler):
def log_message(self, fmt, *args):
print(f"[{datetime.now().strftime('%H:%M:%S')}] {fmt % args}")
get_logger().info(fmt % args)
def _send(self, code: int, body: str, content_type: str = 'text/html; charset=utf-8'):
encoded = body.encode('utf-8')
@@ -129,6 +131,45 @@ def make_handler(db_path: str):
'method': 'POST', 'path': '/settings/login',
'code': 302, 'client': self.client_address[0],
})
elif parsed.path == '/settings/db':
if not _is_authenticated(self.headers.get('Cookie', '')):
self.send_response(302)
self.send_header('Location', '/login')
self.end_headers()
return
length = int(self.headers.get('Content-Length', 0))
body = self.rfile.read(length).decode('utf-8')
fields = {}
for part in body.split('&'):
if '=' in part:
k, v = part.split('=', 1)
fields[unquote_plus(k)] = unquote_plus(v)
conn_type = fields.get('conn_type', 'sqlite').strip().lower()
pg_host = fields.get('pg_host', '').strip()
pg_port = fields.get('pg_port', '5432').strip()
pg_db = fields.get('pg_db', '').strip()
pg_user = fields.get('pg_user', '').strip()
pg_password = fields.get('pg_password', '').strip()
# Keep existing password if field left blank
if not pg_password and _conn['type'] == ConnType.POSTGRES:
pg_password = _conn.get('pg_password', '')
_update_db_conn(conn_type, pg_host, pg_port, pg_db, pg_user, pg_password)
# Update db_path and db_type in _state
if conn_type == 'postgres':
new_path = f"postgresql://{pg_host}:{pg_port}/{pg_db}"
_state['db_path'] = new_path
try:
_refresh_db_type(new_path)
except Exception:
_state['db_type'] = None
self.send_response(302)
self.send_header('Location', '/')
self.end_headers()
_access_log.append({
'ts': datetime.now().strftime('%d/%m/%Y %H:%M:%S'),
'method': 'POST', 'path': '/settings/db',
'code': 302, 'client': self.client_address[0],
})
else:
self._send(405, '<h1>405 Method Not Allowed</h1>')
@@ -191,10 +232,11 @@ def make_handler(db_path: str):
return
if self.path == '/change-db':
chosen = _pick_db_file()
if chosen and os.path.exists(chosen):
_state['db_path'] = chosen
_refresh_db_type(chosen)
if _conn['type'] != ConnType.POSTGRES:
chosen = _pick_db_file()
if chosen and os.path.exists(chosen):
_state['db_path'] = chosen
_refresh_db_type(chosen)
self.send_response(302)
self.send_header('Location', '/')
self.end_headers()
@@ -375,47 +417,63 @@ def run_server(db_path: str | None, host: str = '0.0.0.0', port: int = 8473, db_
_state['db_type'] = None
handler = make_handler(db_path)
server = HTTPServer((host, port), handler)
log = get_logger()
display_host = 'localhost' if host in ('0.0.0.0', '') else host
base = f"http://{display_host}:{port}"
if db_error:
print(f"ATTENZIONE: {db_error}")
print(f"RPA Report server avviato senza DB — pagina di errore su {base}/")
log.warning(f"ATTENZIONE: {db_error}")
log.info(f"RPA Report server avviato senza DB — pagina di errore su {base}/")
else:
print(f"RPA Report server avviato [db: {db_type}]")
print(f" Dashboard : {base}/")
print(f" Processi : {base}/runs")
log.info(f"RPA Report server avviato [db: {db_type}]")
log.info(f" Dashboard : {base}/")
log.info(f" Processi : {base}/runs")
if db_type == DbType.INTRAZ:
print(f" RPA Steps : {base}/steps")
print(f" Iscr. API : {base}/iscrizioni-api")
print(f" SharePoint : {base}/sharepoint")
print(f" Email : {base}/email")
log.info(f" RPA Steps : {base}/steps")
log.info(f" Iscr. API : {base}/iscrizioni-api")
log.info(f" SharePoint : {base}/sharepoint")
log.info(f" Email : {base}/email")
else:
print(f" PEC : {base}/pec")
print(f" Documenti : {base}/documenti")
print(f" Report : {base}/report")
print(f" Schema : {base}/schema")
print(f" Log DB : {base}/logs")
print(f" Server log : {base}/server-logs")
print(f" DB : {db_path}")
print("Ctrl+C per fermare.\n")
log.info(f" PEC : {base}/pec")
log.info(f" Documenti : {base}/documenti")
log.info(f" Report : {base}/report")
log.info(f" Schema : {base}/schema")
log.info(f" Log DB : {base}/logs")
log.info(f" Server log : {base}/server-logs")
log.info(f" DB : {db_path}")
log.info("Ctrl+C per fermare.")
try:
server.serve_forever()
except KeyboardInterrupt:
print("\nServer fermato.")
log.info("Server fermato.")
server.server_close()
def main():
from dotenv import load_dotenv
from pathlib import Path
from .logger import setup_logging
load_dotenv()
db_file = os.environ.get('RPA_DB_FILE', 'rpa_FORMAZIONE.db')
log_dir_env = os.environ.get('RPA_LOG_DIR', '')
log_dir = Path(log_dir_env) if log_dir_env else REPORTS_DIR / 'logs'
setup_logging(log_dir)
db_error = None
try:
db_path = _find_db(db_file)
except FileNotFoundError as exc:
db_path = None
db_error = str(exc)
if _conn['type'] == ConnType.POSTGRES:
db_path = f"postgresql://{_conn['pg_host']}:{_conn['pg_port']}/{_conn['pg_db']}"
try:
from .queries.db import _pg_query
_pg_query("SELECT 1")
except Exception as exc:
db_path = None
db_error = f"Connessione PostgreSQL fallita: {exc}"
else:
db_file = os.environ.get('RPA_DB_FILE', 'rpa_FORMAZIONE.db')
try:
db_path = _find_db(db_file)
except FileNotFoundError as exc:
db_path = None
db_error = str(exc)
port = int(os.environ.get('RPA_REPORT_PORT', 8473))
run_server(db_path, port=port, db_error=db_error)
+1
View File
@@ -1 +1,2 @@
python-dotenv>=1.0.0
psycopg2-binary>=2.9 # optional — only needed for PostgreSQL mode
+103
View File
@@ -0,0 +1,103 @@
#Requires -RunAsAdministrator
<#
.SYNOPSIS
Install (or remove) RpaDashboard as a Windows service via NSSM.
.PARAMETER PythonExe
Full path to python.exe. Leave blank to auto-detect from PATH
(or the active virtual environment if the script is run inside one).
.PARAMETER Uninstall
Stop and remove the service instead of installing it.
.EXAMPLE
.\install_service.ps1
.\install_service.ps1 -PythonExe "C:\Python314\python.exe"
.\install_service.ps1 -Uninstall
#>
param(
[string]$PythonExe = '',
[switch]$Uninstall
)
$ServiceName = 'RpaDashboard'
$RootDir = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)
# --- Uninstall path ---
if ($Uninstall) {
Write-Host "Stopping and removing service '$ServiceName'..."
nssm stop $ServiceName 2>$null
nssm remove $ServiceName confirm
Write-Host "Done."
exit 0
}
# --- Resolve Python executable ---
if (-not $PythonExe) {
$venvPython = Join-Path $RootDir '.venv\Scripts\python.exe'
if (Test-Path $venvPython) {
$PythonExe = $venvPython
} else {
$found = Get-Command python -ErrorAction SilentlyContinue
if ($found) { $PythonExe = $found.Source }
}
}
if (-not $PythonExe -or -not (Test-Path $PythonExe)) {
Write-Error "Python not found. Pass -PythonExe 'C:\path\to\python.exe'"
exit 1
}
if (-not (Get-Command nssm -ErrorAction SilentlyContinue)) {
Write-Error "NSSM not found in PATH. Download from https://nssm.cc and add it to PATH."
exit 1
}
$Script = Join-Path $RootDir 'Dashboard.py'
$LogDir = Join-Path $RootDir 'logs'
New-Item -ItemType Directory -Force -Path $LogDir | Out-Null
Write-Host "Installing service '$ServiceName'..."
Write-Host " Python : $PythonExe"
Write-Host " Script : $Script"
Write-Host " Logs : $LogDir"
Write-Host ""
# Install service
nssm install $ServiceName $PythonExe "-u" $Script
# Working directory (important for .env loading)
nssm set $ServiceName AppDirectory $RootDir
# Display name and description visible in services.msc
nssm set $ServiceName DisplayName 'RPA Dashboard'
nssm set $ServiceName Description 'Dashboard per il monitoraggio dei processi RPA — autodeployed by viXEvo'
# Start automatically with Windows
nssm set $ServiceName Start SERVICE_AUTO_START
# Redirect stdout/stderr (catches crashes before logging initialises)
nssm set $ServiceName AppStdout (Join-Path $LogDir 'service_stdout.log')
nssm set $ServiceName AppStderr (Join-Path $LogDir 'service_stderr.log')
# Rotate NSSM's own stdout/stderr files daily
nssm set $ServiceName AppRotateFiles 1
nssm set $ServiceName AppRotateSeconds 86400
# Restart automatically 5 s after a crash
nssm set $ServiceName AppExit Default Restart
nssm set $ServiceName AppRestartDelay 5000
# Start the service now
nssm start $ServiceName
Write-Host ""
Write-Host "Service '$ServiceName' installed and started."
Write-Host ""
Write-Host "Useful commands:"
Write-Host " Status : Get-Service $ServiceName"
Write-Host " Stop : Stop-Service $ServiceName"
Write-Host " Restart : Restart-Service $ServiceName"
Write-Host " Logs : Get-Content '$LogDir\dashboard.log' -Wait"
Write-Host " Uninstall : .\install_service.ps1 -Uninstall"
+9
View File
@@ -26,6 +26,7 @@
<div class="wrench-drop">
<a href="/change-db">&#128194; Cambia DB&hellip;</a>
<button onclick="document.getElementById('modal-db-types').classList.add('open');this.closest('.wrench-drop').classList.remove('open')">&#128203; Tipi di Dashboard&hellip;</button>
<button onclick="document.getElementById('modal-db-conn').classList.add('open');this.closest('.wrench-drop').classList.remove('open')">&#128225; Sorgente Dati&hellip;</button>
<button onclick="document.getElementById('modal-login-settings').classList.add('open');this.closest('.wrench-drop').classList.remove('open')">&#128274; Impostazioni Login&hellip;</button>
</div>
</div>
@@ -55,6 +56,7 @@ ${content}
</div>
</div>
${modal_login}
${modal_db_conn}
<script>${page_js}</script>
<script>
document.addEventListener('DOMContentLoaded',function(){
@@ -89,6 +91,13 @@ if (_modalLogin) {
});
}
var _modalDbConn = document.getElementById('modal-db-conn');
if (_modalDbConn) {
_modalDbConn.addEventListener('click', function(e) {
if (e.target === this) this.classList.remove('open');
});
}
document.querySelectorAll('th').forEach(function(th, i) {
th.style.cursor = 'pointer';
th.title = 'Clicca per ordinare';