- [ ] Backup of generated configurations
### 🌍 Language
-- [ ] Localization
+- [X] Localization
---
from backend.routes.backup import router as backup_router
from backend.routes.logs import router as logs_router
from backend.routes.settings import router as settings_router
+from backend.routes.localization import router as localization_router
# Import Security
from backend.security import is_logged_in, apply_session
app.include_router(backup_router)
app.include_router(logs_router)
app.include_router(settings_router)
+ app.include_router(localization_router)
# CORS
cors_origins = [
from datetime import datetime, timezone
import hashlib
import json
-import os
from pathlib import Path
+import shutil
import time
from typing import List, Dict, Any, Optional, Union
import zipfile
# Set to True to remove individual backup files after creating the archive (optional, can be set to False for debugging)
remove_backup_files = True
+# ---------------------------------------------------------
+# Internal: Cleanup Extract Directory
+# ---------------------------------------------------------
+def cleanup_extract_dir(path: Path) -> None:
+ try:
+ shutil.rmtree(path, ignore_errors=True)
+ except Exception as e:
+ logger.warning("Failed removing temporary folder %s: %s", path, str(e))
+
# ---------------------------------------------------------
# Internal: Generate Filestamp
# ---------------------------------------------------------
"total": len(operations),
"success": sum(1 for op in operations.values() if op.get("status") == "success"),
"failed": sum(1 for op in operations.values() if op.get("status") == "failure"),
+ "not_found": sum(1 for op in operations.values() if op.get("status") == "not_found"),
}
errors = sum(
except Exception as e:
logger.exception("restore_hosts failed applying records: %s", str(e).strip())
- errors.append(str(e));
+ errors.append(str(e))
took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
"took_ms": took_ms,
}
else:
- count_stored = count_loaded
result: Dict[str, Any] = {
"status": "success",
"file": str(file),
"count_loaded": count_loaded,
- "count_stored": count_stored,
+ "count_restored": count_restored,
"took_ms": took_ms,
}
except Exception as e:
logger.exception("restore_aliases failed applying records: %s", str(e).strip())
- errors.append(str(e));
+ errors.append(str(e))
took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
"took_ms": took_ms,
}
else:
- count_stored = count_loaded
result: Dict[str, Any] = {
"status": "success",
"file": str(file),
"count_loaded": count_loaded,
- "count_stored": count_stored,
+ "count_restored": count_restored,
"took_ms": took_ms,
}
# ---------------------------------------------------------
def backup_create() -> Dict[str, Any]:
+ # Initialization
+ operations = {
+ "metadata": {},
+ "hosts": {},
+ "aliases": {},
+ "archive": {},
+ }
+
# Ensure backup directory exists
base_dir = Path(get_config("BACKUP_PATH"))
base_dir.mkdir(parents=True, exist_ok=True)
# Timestamp used for backup file naming
ts = generate_timestamps()
timestamp = ts["iso"] # per metadata/API
- file_timestamp = ts["file"] # per filename
+ file_timestamp = ts["file"] # per filename
# Create zip folder
zip_name = f"backup_{file_timestamp}.zip"
backup_path=base_dir / f"backup_{file_timestamp}"
backup_path.mkdir(parents=True, exist_ok=True)
- # Init struttura unica
- operations = {
- "metadata": {},
- "hosts": {},
- "aliases": {},
- "archive": {},
- }
-
# --- STEP ---
operations["hosts"] = store_hosts(timestamp=timestamp, filepath=backup_path)
operations["aliases"] = store_aliases(timestamp=timestamp, filepath=backup_path)
# ---------------------------------------------------------
def backup_restore(backup_id: str, cleanup: bool = True) -> Dict[str, Any]:
- # Init struttura unica
+ # Initialization
operations = {
"archive": {},
"metadata": {},
if not backup_file.is_file():
logger.error(f"Backup file not found: {backup_file}")
- raise FileNotFoundError("Backup file not found")
+ operations["archive"] = {
+ "status": "not_found",
+ "errors": [f"Backup file not found: {backup_id}"]
+ }
+ return build_result(operations)
# --- ARCHIVE ---
operations["archive"] = unzip_backup_archive(zip_name=backup_id, extract_dir=extract_dir)
# --- METADATA ---
operations["metadata"] = check_metadata(filepath=extract_dir, remove_file=remove_backup_files)
if operations["metadata"].get("status") != "success":
+ cleanup_extract_dir(extract_dir)
return build_result(operations)
# --- CLEANUP ---
try:
reset_hosts_db()
reset_aliases_db()
+ operations["cleanup"] = {
+ "status": "success"
+ }
except Exception as e:
logger.exception("Cleanup failed %s", str(e).strip())
- raise
+ operations["cleanup"] = {
+ "status": "failure",
+ "errors": [str(e)]
+ }
+ cleanup_extract_dir(extract_dir)
+ return build_result(operations)
# --- RESTORE FILES ---
for f in operations["metadata"]["files"]:
operations["aliases"] = restore_aliases(filepath=extract_dir, filename=f["file"], remove_file=remove_backup_files)
if remove_backup_files:
- p = Path(extract_dir)
- if p.is_dir() and not any(p.iterdir()):
- p.rmdir()
+ cleanup_extract_dir(extract_dir)
return build_result(operations)
def backup_delete(backup_id: str) -> Dict[str, Any]:
# Initialization
+ operations = {
+ "delete": {},
+ }
start_ns = time.monotonic_ns()
- errors: List[str] = []
backup_dir = Path(get_config("BACKUP_PATH"))
backup_file = backup_dir / backup_id
try:
# Check if file exists
if not backup_file.is_file():
- raise FileNotFoundError(f"Backup file not found: {backup_id}")
+ logger.error(f"Backup file not found: {backup_file}")
+ operations["delete"] = {
+ "status": "not_found",
+ "errors": [f"Backup file not found: {backup_id}"]
+ }
+ operations["delete"]["took_ms"] = (time.monotonic_ns() - start_ns) / 1_000_000
+ return build_result(operations)
# Remove file
backup_file.unlink()
+ operations["delete"] = {
+ "status": "success",
+ "file": str(backup_file),
+ }
+
except Exception as e:
logger.exception("delete_backup failed: %s", str(e).strip())
- errors.append(str(e))
+ operations["delete"] = {
+ "status": "failure",
+ "errors": [str(e)],
+ }
- took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
+ operations["delete"]["took_ms"] = (time.monotonic_ns() - start_ns) / 1_000_000
- return {
- "status": "failure" if errors else "success",
- "file": str(backup_file) if not errors else None,
- "errors": errors,
- "took_ms": took_ms,
- }
+ return build_result(operations)
"Database: file=%s | reset=%s",
str(settings.DB_FILE), settings.DB_RESET
)
+ logger.info(
+ "Language: %s",
+ get_config("LANGUAGE")
+ )
logger.info(
"Log: level=%s, to_file=%s, file=%s",
get_config("LOG_LEVEL"), get_config("LOG_TO_FILE"), str(settings.LOG_FILE)
# -----------------------------
# ADD ALIAS
# -----------------------------
-def add_alias(data: Dict[str, Any]) -> int:
+def add_alias(data: Dict[str, Any]):
# Validate input
cleaned = validate_data(data)
),
)
conn.commit()
- return cur.lastrowid
except sqlite3.IntegrityError:
conn.rollback()
if not MAC_RE.match(mac):
raise ValueError(f"Invalid MAC address: {mac}")
- # Check description
+ # Check Description
description = data.get("description")
- # Normalizzazione boolean per DB (0/1)
+ # Boolean normalization for DB (0/1)
ssl_enabled = int(bool(data.get("ssl_enabled", 0)))
- # Normalizzazione (0/1/2)
+ # Normalization (0/1/2)
v = int(data.get("visibility", 0))
visibility = v if v in (0, 1, 2) else 0
# -----------------------------
# ADD HOST
# -----------------------------
-def add_host(data: Dict[str, Any]) -> int:
+def add_host(data: Dict[str, Any]):
# Validate input
cleaned = validate_data(data)
),
)
conn.commit()
- return cur.lastrowid
except sqlite3.IntegrityError:
conn.rollback()
# -----------------------------
# UPDATE HOST
# -----------------------------
-def update_host(host_id: int, data: Dict[str, Any]) -> bool:
+def update_host(host_id: int, data: Dict[str, Any]):
# Validate input
cleaned = validate_data(data)
host_id,
),
)
+
+ if cur.rowcount == 0:
+ raise ValueError(f"Host {host_id} not found")
+
conn.commit()
- return cur.rowcount > 0
except Exception as err:
conn.rollback()
# -----------------------------
# DELETE HOST
# -----------------------------
-def delete_host(host_id: int) -> bool:
+def delete_host(host_id: int):
# Validate input
if host_id is None:
conn = get_db()
try:
cur = conn.execute("DELETE FROM hosts WHERE id = ?", (host_id,))
+
+ if cur.rowcount == 0:
+ raise ValueError(f"Host {host_id} not found")
+
conn.commit()
- return cur.rowcount > 0
except Exception as err:
conn.rollback()
# Default Values
# ---------------------------------------------------------
CONFIG_DEFAULTS = {
+ "LANGUAGE": {
+ "value": settings.LANGUAGE,
+ "description": "Language for the application",
+ "group_name": "localization",
+ "type": "string",
+ "allowed": ["en", "es", "it", "fr", "de"],
+ },
"LOG_LEVEL": {
"value": settings.LOG_LEVEL,
"description": "Logging verbosity level",
+++ /dev/null
-# backend/system.py
-
-# Import standard modules
-from fastapi import APIRouter
-from datetime import datetime, timezone
-import os
-import signal
-import threading
-import time
-
-# Import Settings & Config
-from backend.settings.settings import settings
-from backend.db.settings import get_config
-
-# Create Router
-router = APIRouter()
-
-# ---------------------------------------------------------
-# Get Information
-# ---------------------------------------------------------
-@router.get("/about")
-def about():
- return {
- "app": {
- "name": settings.APP_NAME,
- "version": settings.APP_VERSION,
- },
- "domain": get_config("DOMAIN"),
- "server_time": datetime.now(timezone.utc).isoformat(),
- }
-
-# ---------------------------------------------------------
-# Restart Application
-# ---------------------------------------------------------
-@router.post("/api/restart")
-def restart():
- def do_restart():
- time.sleep(0.5)
- os.kill(os.getpid(), signal.SIGTERM)
-
- threading.Thread(target=do_restart, daemon=True).start()
-
- return {"message": "Application restarting..."}
# backend/routes/aliases.py
# import standard modules
-from fastapi import APIRouter, Request, Response, HTTPException, status
+from fastapi import APIRouter, HTTPException, status
from fastapi.responses import FileResponse
-import ipaddress
import time
# Import local modules
# ---------------------------------------------------------
# Aliass page
@router.get("/aliases")
-def aliases_page(request: Request):
+def aliases_page():
return FileResponse(settings.FRONTEND_PATH / "aliases.html")
# Serve aliases.js
200: {"description": "Aliass found"},
500: {"description": "Internal server error"},
})
-def api_get_aliases(request: Request):
+def api_get_aliases():
+
try:
aliases = get_aliases()
return aliases or []
- except HTTPException:
- raise
-
except Exception as err:
logger.exception("Error getting list aliases %s", str(err).strip())
raise HTTPException(
404: {"description": "Alias not found"},
500: {"description": "Internal server error"},
})
-def api_get_alias(request: Request, alias_id: int):
+def api_get_alias(alias_id: int):
# Inizializzazioni
start_ns = time.monotonic_ns()
try:
alias = get_alias(alias_id)
- if not alias: # None or empty dict
- took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
- raise HTTPException(
- status_code=status.HTTP_404_NOT_FOUND,
- detail={
- "code": "ALIAS_NOT_FOUND",
- "status": "failure",
- "message": "Alias not found",
- "details": {
- "alias_id": alias_id,
- "took_ms": took_ms,
- },
- },
- )
- return alias
-
- except HTTPException:
- raise
except Exception as err:
logger.exception("Error getting alias %s: %s", alias_id, str(err).strip())
},
)
+ if not alias: # None or empty dict
+ took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail={
+ "code": "ALIAS_NOT_FOUND",
+ "status": "failure",
+ "message": "Alias not found",
+ "details": {
+ "alias_id": alias_id,
+ "took_ms": took_ms,
+ },
+ },
+ )
+
+ return alias
+
# ---------------------------------------------------------
# Add Aliass
# ---------------------------------------------------------
-@router.post("/api/aliases", status_code=status.HTTP_200_OK, responses={
- 200: {"description": "Alias added"},
+@router.post("/api/aliases", status_code=status.HTTP_201_CREATED, responses={
+ 201: {"description": "Alias added"},
409: {"description": "Alias already present"},
500: {"description": "Internal server error"},
})
-def api_add_alias(request: Request, data: dict):
+def api_add_alias(data: dict):
# Inizializzazioni
start_ns = time.monotonic_ns()
+ alias_id = None
try:
alias_id = add_alias(data)
- if(alias_id > 0):
- took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
- return {
- "code": "ALIAS_ADDED",
- "status": "success",
- "message": "Alias added successfully",
- "details": {
- "alias_id": alias_id,
- "took_ms": took_ms,
- },
- }
+ took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
+ return {
+ "code": "ALIAS_ADDED",
+ "status": "success",
+ "message": "Alias added successfully",
+ "details": {
+ "alias_id": alias_id,
+ "took_ms": took_ms,
+ },
+ }
- # Already present
+ # Not Found
+ except ValueError:
took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
},
)
- except HTTPException:
- raise
-
except Exception as err:
logger.exception("Error adding alias: %s", str(err).strip())
took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
404: {"description": "Alias not found"},
500: {"description": "Internal server error"},
})
-def api_update_alias(request: Request, data: dict, alias_id: int):
+def api_update_alias(data: dict, alias_id: int):
# Inizializzazioni
start_ns = time.monotonic_ns()
try:
- updated = update_alias(alias_id, data)
- if updated:
- took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
- return {
- "code": "ALIAS_UPDATED",
- "status": "success",
- "message": "Alias updated successfully",
- "details": {
- "alias_id": alias_id,
- "took_ms": took_ms,
- },
- }
+ update_alias(alias_id, data)
+ took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
+ return {
+ "code": "ALIAS_UPDATED",
+ "status": "success",
+ "message": "Alias updated successfully",
+ "details": {
+ "alias_id": alias_id,
+ "took_ms": took_ms,
+ },
+ }
- # Not Found
+ # Not Found
+ except ValueError:
took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
},
)
- except HTTPException:
- raise
-
except Exception as err:
logger.exception("Error updating alias %s: %s", alias_id, str(err).strip())
took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
404: {"description": "Alias not found"},
500: {"description": "Internal server error"},
})
-def api_delete_alias(request: Request, alias_id: int):
+def api_delete_alias(alias_id: int):
# Inizializzazioni
start_ns = time.monotonic_ns()
try:
- deleted = delete_alias(alias_id)
- if deleted:
- took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
- return {
- "code": "ALIAS_DELETED",
- "status": "success",
- "message": "Alias deleted successfully",
- "details": {
- "alias_id": alias_id,
- "took_ms": took_ms,
- },
- }
+ delete_alias(alias_id)
+ took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
+ return {
+ "code": "ALIAS_DELETED",
+ "status": "success",
+ "message": "Alias deleted successfully",
+ "details": {
+ "alias_id": alias_id,
+ "took_ms": took_ms,
+ },
+ }
- # Not Found
+ # Not Found
+ except ValueError:
took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
},
)
- except HTTPException:
- raise
-
except Exception as err:
logger.exception("Error deleting alias %s: %s", alias_id, str(err).strip())
took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
# Import local modules
from backend.backup import backup_create, backup_list, backup_restore, backup_delete
-# Import Settings & Config
-from backend.settings.settings import settings
+# Import Config
from backend.db.settings import get_config
# Import Logging
from backend.log.log import get_logger
def build_operation_response(
*,
code_ok: str,
+ code_not_found: str,
code_error: str,
message_ok: str,
+ message_not_found: str,
message_partial: str,
message_error: str,
result: dict,
total = summary.get("total", 0)
success = summary.get("success", 0)
failed = summary.get("failed", 0)
+ not_found = summary.get("not_found", 0)
took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
+ # Handle not found case
+ if not_found > 0:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail={
+ "code": code_not_found,
+ "status": "not_found",
+ "message": message_not_found,
+ "took_ms": took_ms,
+ "results": result,
+ },
+ )
+
+ # Handle partial success case
is_partial = failed > 0 or success != total
if is_partial:
if success > 0:
result = backup_create()
return build_operation_response(
- code_ok="BACKUP_CREATE_OK",
+ code_ok="BACKUP_CREATED",
+ code_not_found="BACKUP_NOT_FOUND",
code_error="BACKUP_CREATE_ERROR",
message_ok="Backup executed successfully",
message_partial="Backup completed with some failed operations",
+ message_not_found="Backup not found",
message_error="Some operations failed",
result=result,
start_ns=start_ns,
"backups": backups
}
- except HTTPException:
- raise
-
except Exception as err:
logger.exception("Error listing backups: %s", str(err))
raise HTTPException(
result = backup_restore(backup_id=payload.backup_id)
return build_operation_response(
- code_ok="BACKUP_RESTORE_OK",
+ code_ok="BACKUP_RESTORED",
+ code_not_found="BACKUP_NOT_FOUND",
code_error="BACKUP_RESTORE_ERROR",
message_ok="Restore executed successfully",
message_partial="Restore completed with some failed operations",
+ message_not_found="Backup not found",
message_error="Some operations failed",
result=result,
start_ns=start_ns,
try:
# Delete Backup
result = backup_delete(backup_id=payload.backup_id)
- took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
-
- if result.get("status") != "success":
- raise HTTPException(
- status_code=status.HTTP_404_NOT_FOUND,
- detail={
- "code": "BACKUP_NOT_FOUND",
- "status": "failure",
- "message": "Backup not found",
- "took_ms": took_ms,
- "results": result,
- },
- )
- return {
- "code": "BACKUP_DELETED",
- "status": "success",
- "message": "Backup deleted successfully",
- "took_ms": took_ms,
- "results": result,
- }
+ return build_operation_response(
+ code_ok="BACKUP_DELETED",
+ code_not_found="BACKUP_NOT_FOUND",
+ code_error="BACKUP_DELETE_ERROR",
+ message_ok="Backup deleted successfully",
+ message_partial="Backup deletion completed with some failed operations",
+ message_not_found="Backup not found",
+ message_error="Some operations failed",
+ result=result,
+ start_ns=start_ns,
+ )
except HTTPException:
raise
def download_backup(backup_id: str):
backup_dir = Path(get_config("BACKUP_PATH"))
- zip_path = backup_dir / f"{backup_id}"
+ zip_path = backup_dir / backup_id
if not zip_path.exists():
raise HTTPException(
- status_code=status.HTTP_404_NOT_FOUND,
- detail={
- "code": "BACKUP_NOT_FOUND",
- "status": "failure",
- "message": "Backup not found",
- },
- )
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail={
+ "code": "BACKUP_NOT_FOUND",
+ "status": "not_found",
+ "message": "Backup not found",
+ },
+ )
return FileResponse(
path=zip_path,
)
# validate ZIP
- import zipfile
try:
with zipfile.ZipFile(file.file) as z:
if z.testzip() is not None:
# import standard modules
from concurrent.futures import ThreadPoolExecutor
-from fastapi import APIRouter, Request, Response, HTTPException, status
+from fastapi import APIRouter, HTTPException, status
from fastapi.responses import FileResponse
-import ipaddress
-import time
# Import local modules
from backend.db.hosts import get_hosts
# ---------------------------------------------------------
# Devices page
@router.get("/devices")
-def devices_page(request: Request):
+def devices_page():
return FileResponse(settings.FRONTEND_PATH / "devices.html")
# Serve devices.js
200: {"description": "Devices found"},
500: {"description": "Internal server error"},
})
-def api_get_devices(request: Request):
+def api_get_devices():
try:
+ workers = get_config("PING_WORKERS")
hosts = get_hosts(filter_devices=True)
- with ThreadPoolExecutor(max_workers=get_config("PING_WORKERS")) as executor:
- futures = [executor.submit(is_host_active, host["ipv4"]) for host in hosts]
- for i, future in enumerate(futures):
- hosts[i]["dhcp_state"] = "static"
- hosts[i]["active"] = future.result()
-
leases = get_leases(filter_devices=True)
- with ThreadPoolExecutor(max_workers=get_config("PING_WORKERS")) as executor:
- futures = [executor.submit(is_host_active, lease["ipv4"]) for lease in leases]
- for i, future in enumerate(futures):
- leases[i]["description"] = None
- leases[i]["active"] = future.result()
- return hosts+leases or []
+ for host in hosts:
+ host["dhcp_state"] = "static"
+
+ for lease in leases:
+ lease["description"] = None
+
+ devices = hosts + leases
+
+ with ThreadPoolExecutor(max_workers=workers) as executor:
+ futures = [
+ executor.submit(is_host_active, device["ipv4"])
+ for device in devices
+ ]
+
+ for i, future in enumerate(futures):
+ devices[i]["active"] = future.result()
- except HTTPException:
- raise
+ return devices
except Exception as err:
logger.exception("Error getting list devices %s", str(err).strip())
# backend/routes/dhcp.py
# import standard modules
-from fastapi import APIRouter, Request, Response, HTTPException, status
+from fastapi import APIRouter, HTTPException, status
from fastapi.responses import FileResponse
import json
from pathlib import Path
# ---------------------------------------------------------
# Leases page
@router.get("/leases")
-def leases_page(request: Request):
+def leases_page():
return FileResponse(settings.FRONTEND_PATH / "leases.html")
# Serve leases.js
200: {"description": "DHCP configuration reload successfully"},
500: {"description": "Internal server error"},
})
-async def api_dhcp_reload(request: Request):
+async def api_dhcp_reload():
# Inizializzazioni
start_ns = time.monotonic_ns()
404: {"description": "Leases not found"},
500: {"description": "Internal server error"},
})
-def api_dhcp_leases(request: Request):
+def api_dhcp_leases():
try:
leases = get_leases()
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail={
- "code": "DHCP_LEASES_NOT_FOUND",
+ "code": "DHCP_LEASE_NOT_FOUND",
"status": "failure",
"message": str(err),
},
404: {"description": "Lease not found"},
500: {"description": "Internal server error"},
})
-def api_get_lease(request: Request, lease_id: int):
+def api_get_lease(lease_id: int):
# Inizializzazioni
start_ns = time.monotonic_ns()
404: {"description": "Lease not found"},
500: {"description": "Internal server error"},
})
-def api_delete_lease(request: Request, lease_id: int):
+def api_delete_lease(lease_id: int):
# Inizializzazioni
start_ns = time.monotonic_ns()
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail={
- "code": "DHCP_LEASES_NOT_FOUND",
+ "code": "DHCP_LEASE_NOT_FOUND",
"status": "failure",
"message": str(err),
"details": {
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail={
- "code": "DHCP_LEASES_NOT_FOUND",
+ "code": "DHCP_LEASE_NOT_FOUND",
"status": "failure",
"message": str(err),
"details": {
# backend/routes/dns.py
# import standard modules
-from fastapi import APIRouter, Request, Response, HTTPException, status
+from fastapi import APIRouter, HTTPException, status
from fastapi.responses import FileResponse
import asyncio
import json
200: {"description": "DNS configuration reload successfully"},
500: {"description": "Internal server error"},
})
-async def api_dns_reload(request: Request):
+async def api_dns_reload():
# Inizializzazioni
start_ns = time.monotonic_ns()
def health():
start = time.time()
- db_status = "ok"
+ db_status = "healthy"
db_version = None
db_tables = None
db_size = None
latency = round((time.time() - start) * 1000, 2)
return {
- "status": "ok" if db_status == "ok" else "degraded",
+ "status": "healthy" if db_status == "healthy" else "degraded",
"latency_ms": latency,
"database": {
"status": db_status,
# backend/routes/hosts.py
# import standard modules
-from fastapi import APIRouter, Request, Response, HTTPException, status
+from fastapi import APIRouter, HTTPException, status
from fastapi.responses import FileResponse
-import ipaddress
import time
# Import local modules
# ---------------------------------------------------------
# Hosts page
@router.get("/hosts")
-def hosts_page(request: Request):
+def hosts_page():
return FileResponse(settings.FRONTEND_PATH / "hosts.html")
# Serve hosts.js
200: {"description": "Hosts found"},
500: {"description": "Internal server error"},
})
-def api_get_hosts(request: Request):
+def api_get_hosts():
try:
hosts = get_hosts()
return hosts or []
- except HTTPException:
- raise
-
except Exception as err:
logger.exception("Error getting list hosts %s", str(err).strip())
raise HTTPException(
detail={
"code": "HOSTS_GET_ERROR",
"status": "failure",
- "message": "Internal error getting host",
+ "message": "Internal error getting hosts",
},
)
404: {"description": "Host not found"},
500: {"description": "Internal server error"},
})
-def api_get_host(request: Request, host_id: int):
+def api_get_host(host_id: int):
# Inizializzazioni
start_ns = time.monotonic_ns()
try:
host = get_host(host_id)
- if not host: # None or empty dict
- took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
- raise HTTPException(
- status_code=status.HTTP_404_NOT_FOUND,
- detail={
- "code": "HOST_NOT_FOUND",
- "status": "failure",
- "message": "Host not found",
- "details": {
- "host_id": host_id,
- "took_ms": took_ms,
- },
- },
- )
- return host
-
- except HTTPException:
- raise
except Exception as err:
logger.exception("Error getting host %s: %s", host_id, str(err).strip())
},
)
+ if not host: # None or empty dict
+ took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail={
+ "code": "HOST_NOT_FOUND",
+ "status": "failure",
+ "message": "Host not found",
+ "details": {
+ "host_id": host_id,
+ "took_ms": took_ms,
+ },
+ },
+ )
+
+ return host
+
# ---------------------------------------------------------
# Add Hosts
# ---------------------------------------------------------
-@router.post("/api/hosts", status_code=status.HTTP_200_OK, responses={
- 200: {"description": "Host added"},
+@router.post("/api/hosts", status_code=status.HTTP_201_CREATED, responses={
+ 201: {"description": "Host added"},
409: {"description": "Host already present"},
500: {"description": "Internal server error"},
})
-def api_add_host(request: Request, data: dict):
+def api_add_host(data: dict):
# Inizializzazioni
start_ns = time.monotonic_ns()
+ host_id = None
try:
host_id = add_host(data)
- if(host_id > 0):
- took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
- return {
- "code": "HOST_ADDED",
- "status": "success",
- "message": "Host added successfully",
- "details": {
- "host_id": host_id,
- "took_ms": took_ms,
- },
- }
+ took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
+ return {
+ "code": "HOST_ADDED",
+ "status": "success",
+ "message": "Host added successfully",
+ "details": {
+ "host_id": host_id,
+ "took_ms": took_ms,
+ },
+ }
- # Already present
+ # Not Found
+ except ValueError:
took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
},
)
- except HTTPException:
- raise
-
except Exception as err:
logger.exception("Error adding host: %s", str(err).strip())
took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
404: {"description": "Host not found"},
500: {"description": "Internal server error"},
})
-def api_update_host(request: Request, data: dict, host_id: int):
+def api_update_host(data: dict, host_id: int):
# Inizializzazioni
start_ns = time.monotonic_ns()
try:
- updated = update_host(host_id, data)
- if updated:
- took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
- return {
- "code": "HOST_UPDATED",
- "status": "success",
- "message": "Host updated successfully",
- "details": {
- "host_id": host_id,
- "took_ms": took_ms,
- },
- }
+ update_host(host_id, data)
+ took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
+ return {
+ "code": "HOST_UPDATED",
+ "status": "success",
+ "message": "Host updated successfully",
+ "details": {
+ "host_id": host_id,
+ "took_ms": took_ms,
+ },
+ }
- # Not Found
+ # Not Found
+ except ValueError:
took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
},
)
- except HTTPException:
- raise
-
except Exception as err:
logger.exception("Error updating host %s: %s", host_id, str(err).strip())
took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
404: {"description": "Host not found"},
500: {"description": "Internal server error"},
})
-def api_delete_host(request: Request, host_id: int):
+def api_delete_host(host_id: int):
# Inizializzazioni
start_ns = time.monotonic_ns()
try:
- deleted = delete_host(host_id)
- if deleted:
- took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
- return {
- "code": "HOST_DELETED",
- "status": "success",
- "message": "Host deleted successfully",
- "details": {
- "host_id": host_id,
- "took_ms": took_ms,
- },
- }
+ delete_host(host_id)
+ took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
+ return {
+ "code": "HOST_DELETED",
+ "status": "success",
+ "message": "Host deleted successfully",
+ "details": {
+ "host_id": host_id,
+ "took_ms": took_ms,
+ },
+ }
- # Not Found
+ # Not Found
+ except ValueError:
took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
},
)
- except HTTPException:
- raise
-
except Exception as err:
logger.exception("Error deleting host %s: %s", host_id, str(err).strip())
took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
--- /dev/null
+# backend/routes/localization.py
+
+# import standard modules
+from fastapi import APIRouter, Request, Response, HTTPException, status
+from fastapi.responses import FileResponse
+
+# Import Settings & Config
+from backend.settings.settings import settings
+from backend.db.settings import get_config
+# Import Logging
+from backend.log.log import get_logger
+# Import Localization
+from backend.settings.localization import load_language
+
+# Logger initialization
+logger = get_logger(__name__)
+
+# Create Router
+router = APIRouter()
+
+# ---------------------------------------------------------
+# FRONTEND PATHS (absolute paths inside Docker)
+# ---------------------------------------------------------
+# Serve i18n.js
+@router.get("/js/i18n.js")
+def i18n_js(request: Request):
+ return FileResponse(settings.FRONTEND_PATH / "js/i18n.js")
+
+# Serve backendMessages.js
+@router.get("/js/backendMessages.js")
+def i18n_js(request: Request):
+ return FileResponse(settings.FRONTEND_PATH / "js/backendMessages.js")
+
+# ---------------------------------------------------------
+# Returns the translation dictionary
+# ---------------------------------------------------------
+@router.get("/api/i18n",
+ status_code=status.HTTP_200_OK,
+ summary="Get UI translations",
+ description="Returns the translation dictionary for the currently configured application language.",
+ tags=["Localization"]
+)
+def get_translations():
+ lang = get_config("LANGUAGE")
+ return load_language(lang)
+
+# ---------------------------------------------------------
+# Returns a specific translation dictionary
+# ---------------------------------------------------------
+@router.get(
+ "/api/i18n/{lang}",
+ status_code=status.HTTP_200_OK,
+ responses={
+ 200: {"description": "Translations loaded"},
+ 404: {"description": "Language not found"},
+ 500: {"description": "Internal server error"},
+ },
+ summary="Get translations",
+ description="Returns all translations for the specified language.",
+ tags=["Localization"],
+)
+def get_translations(lang: str):
+ try:
+ translations = load_language(lang, fallback=False)
+ return translations
+
+ except FileNotFoundError:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail={
+ "code": "LANGUAGE_NOT_FOUND",
+ "status": "failure",
+ "message": f"Language '{lang}' not found",
+ },
+ )
+
+ except Exception as err:
+ logger.exception("Error loading translations: %s", str(err).strip())
+
+ raise HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ detail={
+ "code": "TRANSLATION_ERROR",
+ "status": "failure",
+ "message": "Internal error loading translations",
+ },
+ )
\ No newline at end of file
# backend/routes/settings.py
# import standard modules
-from fastapi import APIRouter, Request, Response, HTTPException, status
+from fastapi import APIRouter, HTTPException, status
from fastapi.responses import FileResponse
-import ipaddress
import time
# Import local modules
# ---------------------------------------------------------
# Settings page
@router.get("/settings")
-def settings_page(request: Request):
+def settings_page():
return FileResponse(settings.FRONTEND_PATH / "settings.html")
# Serve settings.js
200: {"description": "Settings found"},
500: {"description": "Internal server error"},
})
-def api_get_configs(request: Request):
+def api_get_configs():
try:
configs = get_configs()
return configs or []
- except HTTPException:
- raise
-
except Exception as err:
logger.exception("Error getting list of the configuration parameters %s", str(err).strip())
raise HTTPException(
404: {"description": "Configuration parameter not found"},
500: {"description": "Internal server error"},
})
-def api_get_setting(request: Request, config_key: str):
+def api_get_setting(config_key: str):
# Inizializzazioni
start_ns = time.monotonic_ns()
try:
config = get_config(config_key, json_format=True)
- if not config: # None or empty dict
- took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
- raise HTTPException(
- status_code=status.HTTP_404_NOT_FOUND,
- detail={
- "code": "CONFIG_NOT_FOUND",
- "status": "failure",
- "message": "Configuration parameter not found",
- "details": {
- "config_key": config_key,
- "took_ms": took_ms,
- },
- },
- )
- return config
-
- except HTTPException:
- raise
except Exception as err:
logger.exception("Error getting configuration parameter %s: %s", config_key, str(err).strip())
},
)
+ if not config: # None or empty dict
+ took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail={
+ "code": "CONFIG_NOT_FOUND",
+ "status": "failure",
+ "message": "Configuration parameter not found",
+ "details": {
+ "config_key": config_key,
+ "took_ms": took_ms,
+ },
+ },
+ )
+ return config
+
# ---------------------------------------------------------
# Update config
# ---------------------------------------------------------
-@router.put("/api/settings/{config_key}", status_code=status.HTTP_200_OK, responses={
- 200: {"description": "Configuration parameter updated"},
+@router.put("/api/settings/{config_key}", status_code=status.HTTP_201_CREATED, responses={
+ 201: {"description": "Configuration parameter updated"},
400: {"description": "Invalid request"},
404: {"description": "Configuration parameter not found"},
500: {"description": "Internal server error"},
})
-def api_update_setting(request: Request, data: dict, config_key: str):
+def api_update_setting(data: dict, config_key: str):
# Inizializzazioni
start_ns = time.monotonic_ns()
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail={
- "code": "CONFIG_RESET_ERROR",
+ "code": "CONFIG_UPDATE_ERROR",
"status": "failure",
"message": (result.get("message") if result else None) or "Internal error updating configuration parameter",
"details": {
# Reset config to default
# ---------------------------------------------------------
@router.post("/api/settings/{config_key}/reset", status_code=status.HTTP_200_OK, responses={
- 200: {"description": "Configuration parameter reset to default"},
+ 200: {"description": "Configuration parameter restored to default"},
400: {"description": "Invalid request"},
404: {"description": "Configuration parameter not found"},
500: {"description": "Internal server error"},
})
-def api_reset_config(request: Request, config_key: str):
+def api_reset_config(config_key: str):
# Inizializzazioni
start_ns = time.monotonic_ns()
if result["status"] == "success":
took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
return {
- "code": "CONFIG_RESET",
+ "code": "CONFIG_RESTORED",
"status": "success",
- "message": "Configuration parameter reset to default successfully",
+ "message": "Configuration parameter restored to default successfully",
"details": {
"config_key": config_key,
"took_ms": took_ms,
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail={
- "code": "CONFIG_RESET_ERROR",
+ "code": "CONFIG_RESTORED_ERROR",
"status": "failure",
- "message": (result.get("message") if result else None) or "Internal error resetting configuration parameter",
+ "message": (result.get("message") if result else None) or "Internal error restoring configuration parameter",
"details": {
"config_key": config_key,
"took_ms": took_ms,
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail={
- "code": "CONFIG_RESET_ERROR",
+ "code": "CONFIG_RESTORED_ERROR",
"status": "failure",
- "message": "Internal error resetting configuration parameter",
+ "message": "Internal error restoring configuration parameter",
"details": {
"config_key": config_key,
"took_ms": took_ms,
# ---------------------------------------------------------
@router.post("/api/restart")
def restart():
+
def do_restart():
time.sleep(0.5)
os.kill(os.getpid(), signal.SIGTERM)
- threading.Thread(target=do_restart, daemon=True).start()
+ threading.Thread(
+ target=do_restart,
+ daemon=True
+ ).start()
- return {"message": "Application restarting..."}
+ return {
+ "code": "APP_RESTARTING",
+ "status": "success",
+ "message": "Application restarting..."
+ }
DB_FILE = "database.db"
DB_RESET = False
+# ---------------------------------------------------------
+# Language
+# ---------------------------------------------------------
+LANGUAGE = "en"
+
# ---------------------------------------------------------
# Log
# ---------------------------------------------------------
--- /dev/null
+# backend/settings/localization.py
+
+# Import standard modules
+import json
+
+# Import Settings
+from backend.settings.settings import settings
+from backend.settings import default
+
+# Import Logging
+from backend.log.log import get_logger
+
+# Logger initialization
+logger = get_logger(__name__)
+
+LOCALES_DIR = settings.FRONTEND_PATH / "locales"
+
+_translations = {}
+
+def load_language(lang: str, fallback: bool = True):
+ if lang in _translations:
+ return _translations[lang]
+
+ file_path = LOCALES_DIR / f"{lang}.json"
+
+ try:
+ with open(file_path, encoding="utf-8") as f:
+ _translations[lang] = json.load(f)
+
+ except (FileNotFoundError, json.JSONDecodeError):
+ if fallback and lang != default.LANGUAGE:
+ logger.warning("Invalid language '%s', using default '%s'", lang, default.LANGUAGE)
+ return load_language(default.LANGUAGE, False)
+
+ raise
+
+ return _translations[lang]
+
+def t(key: str, lang: str):
+ translations = load_language(lang)
+ return translations.get(key, key)
DB_FILE: Path = Field(default_factory=lambda: Path(os.getenv("DB_FILE", default.DB_FILE)))
DB_RESET: bool = Field(default_factory=lambda: to_bool(os.getenv("DB_RESET"), default.DB_RESET))
+ # Language
+ LANGUAGE: str = Field(default_factory=lambda: os.getenv("LANGUAGE", default.LANGUAGE))
+
# Log
LOG_LEVEL: str = Field(default_factory=lambda: os.getenv("LOG_LEVEL", default.LOG_LEVEL))
LOG_TO_FILE: bool = Field(default_factory=lambda: to_bool(os.getenv("LOG_TO_FILE"), default.LOG_TO_FILE))
<link rel="stylesheet" href="css/layout.css">
</head>
-<body>
+<body data-page-title="dashboard.title">
<!-- Topbar -->
<header class="topbar">
<div class="topbar-inner">
<line x1="12" y1="6" x2="12" y2="18" stroke="var(--accent)" stroke-width="2"></line>
<line x1="6" y1="12" x2="18" y2="12" stroke="var(--accent)" stroke-width="2"></line>
</svg>
- <span>Network Manager</span>
+ <span data-i18n="app.name">Network Manager</span>
</a>
<nav class="navbar">
<!-- Desktop nav -->
<div class="d-none d-md-flex gap-2 ms-auto">
- <a href="/hosts" class="btn btn-primary"> Hosts</a>
- <a href="/aliases" class="btn btn-primary active">Alias</a>
- <a href="/leases" class="btn btn-primary"> DHCP Leases</a>
- <a href="/devices" class="btn btn-primary"> Devices</a>
- <a href="/logs" class="btn btn-primary"> Logs</a>
- <a href="/settings" class="btn btn-primary"> Settings</a>
- <button class="btn btn-primary btn-logout">Logout</button>
+ <a href="/hosts" class="btn btn-primary" data-i18n="hosts.title">Hosts</a>
+ <a href="/aliases" class="btn btn-primary active" data-i18n="aliases.title">Alias</a>
+ <a href="/leases" class="btn btn-primary" data-i18n="dhcp.title">DHCP Leases</a>
+ <a href="/devices" class="btn btn-primary" data-i18n="devices.title">Devices</a>
+ <a href="/logs" class="btn btn-primary" data-i18n="logs.title">Logs</a>
+ <a href="/settings" class="btn btn-primary" data-i18n="settings.title">Settings</a>
+ <button class="btn btn-primary btn-logout" data-i18n="auth.logout">Logout</button>
</div>
</nav>
<div class="offcanvas offcanvas-end d-md-none" tabindex="-1" id="mobileMenu">
<div class="offcanvas-header">
- <h5 class="offcanvas-title">Network Manager</h5>
+ <h5 class="offcanvas-title" data-i18n="app.name">Network Manager</h5>
<button
type="button"
class="btn-close"
+ data-i18n-title="common.close"
+ data-i18n-aria-label="common.close"
data-bs-dismiss="offcanvas">
</button>
</div>
<div class="offcanvas-body">
<div class="d-grid gap-2">
- <a href="/hosts" class="btn btn-primary"> Hosts</a>
- <a href="/aliases" class="btn btn-primary active">Alias</a>
- <a href="/leases" class="btn btn-primary"> DHCP Leases</a>
- <a href="/devices" class="btn btn-primary"> Devices</a>
- <a href="/logs" class="btn btn-primary"> Logs</a>
- <a href="/settings" class="btn btn-primary"> Settings</a>
- <button class="btn btn-primary btn-logout">Logout</button>
+ <a href="/hosts" class="btn btn-primary" data-i18n="hosts.title">Hosts</a>
+ <a href="/aliases" class="btn btn-primary" active data-i18n="aliases.title">Alias</a>
+ <a href="/leases" class="btn btn-primary" data-i18n="dhcp.title">DHCP Leases</a>
+ <a href="/devices" class="btn btn-primary" data-i18n="devices.title">Devices</a>
+ <a href="/logs" class="btn btn-primary" data-i18n="logs.title">Logs</a>
+ <a href="/settings" class="btn btn-primary" data-i18n="settings.title">Settings</a>
+ <button class="btn btn-primary btn-logout" data-i18n="auth.logout">Logout</button>
</div>
</div>
</div>
<div class="col-auto col-md-auto">
<h2 class="mb-0 d-flex align-items-center gap-2 lh-1">
<span class="title-icon"><i class="bi bi-diagram-2"></i></span>
- <span class="section-title">Aliases List</span>
+ <span class="section-title" data-i18n="aliases.page.title">Aliases List</span>
</h2>
</div>
<input
type="text"
id="searchInput"
- placeholder="Search..."
class="form-control form-control-sm placeholder-italic"
- aria-label="Search alias">
+ data-i18n-placeholder="common.search.placeholder"
+ data-i18n-aria-label="aliases.search">
</div>
</div>
<!-- Add host Desktop -->
<button
class="btn btn-primary d-none d-md-flex align-items-center gap-2 px-3"
- title="Add Alias"
- aria-label="Add Alias"
+ data-i18n-title="aliases.add"
+ data-i18n-aria-label="aliases.add"
data-bs-toggle="modal" data-bs-target="#addAliasModal">
<i class="bi bi-plus-lg"></i>
- <span class="label">Add Alias</span>
+ <span class="label" data-i18n="aliases.add">Add Alias</span>
</button>
<!-- Separator -->
<!-- Reload DNS Desktop -->
<button
class="btn btn-primary d-none d-md-flex align-items-center gap-2 px-3"
- title="Reload DNS (BIND)"
- aria-label="Reload DNS"
+ data-i18n-title="dns.reload.description"
+ data-i18n-aria-label="dns.reload.description"
data-action="reloadDns">
<i class="bi bi-arrow-repeat"></i>
- <span class="label">Reload DNS</span>
+ <span class="label" data-i18n="dns.reload">Reload DNS</span>
</button>
<!-- Reload DHCP Desktop -->
<button
class="btn btn-primary d-none d-md-flex align-items-center gap-2 px-3"
- title="Reload DHCP (Kea)"
- aria-label="Reload DHCP"
+ data-i18n-title="dhcp.reload.description"
+ data-i18n-aria-label="dhcp.reload.description"
data-action="reloadDhcp">
<i class="bi bi-arrow-repeat"></i>
- <span class="label">Reload DHCP</span>
+ <span class="label" data-i18n="dhcp.reload">Reload DHCP</span>
</button>
<!-- Mobile Dropdown -->
<li class="px-2 py-1">
<button
class="btn btn-primary w-100 d-flex align-items-center gap-2"
- title="Add Alias"
- aria-label="Add Alias"
+ data-i18n-title="aliases.add"
+ data-i18n-aria-label="aliases.add"
data-bs-toggle="modal" data-bs-target="#addAliasModal">
<i class="bi bi-plus-lg"></i>
- <span>Add Alias</span>
+ <span data-i18n="aliases.add">Add Alias</span>
</button>
</li>
<li class="px-2 py-1">
<button
class="btn btn-primary w-100 d-flex align-items-center gap-2"
- title="Reload DNS (BIND)"
- aria-label="Reload DNS"
+ data-i18n-title="dns.reload.description"
+ data-i18n-aria-label="dns.reload.description"
data-action="reloadDns">
<i class="bi bi-arrow-repeat"></i>
- <span>Reload DNS</span>
+ <span data-i18n="dns.reload">Reload DNS</span>
</button>
</li>
<li class="px-2 py-1">
<button
class="btn btn-primary w-100 d-flex align-items-center gap-2"
- title="Reload DHCP (Kea)"
- aria-label="Reload DHCP"
+ data-i18n-title="dhcp.reload.description"
+ data-i18n-aria-label="dhcp.reload.description"
data-action="reloadDhcp">
<i class="bi bi-arrow-repeat"></i>
- <span>Reload DHCP</span>
+ <span data-i18n="dhcp.reload">Reload DHCP</span>
</button>
</li>
</ul>
<input
type="text"
id="searchInputMobile"
- placeholder="Search..."
class="form-control form-control-sm placeholder-italic"
- aria-label="Search alias">
+ data-i18n-placeholder="common.search.placeholder"
+ data-i18n-aria-label="aliases.search">
</div>
</div>
</section>
<thead class="table-light">
<tr>
<th data-type="string" data-sortable="true" data-sort="0">
- Alias <span class="sort-arrow" aria-hidden="true"></span>
+ <span data-i18n="aliases.name">Alias</span>
+ <span class="sort-arrow" aria-hidden="true"></span>
</th>
<th data-type="string" data-sortable="true" data-sort="1">
- Target <span class="sort-arrow" aria-hidden="true"></span>
+ <span data-i18n="aliases.target">Target</span>
+ <span class="sort-arrow" aria-hidden="true"></span>
</th>
<th data-type="string" data-sortable="true" data-sort="2">
- Description <span class="sort-arrow" aria-hidden="true"></span>
+ <span data-i18n="common.description">Description</span>
+ <span class="sort-arrow" aria-hidden="true"></span>
</th>
<th data-type="string" data-sortable="true" data-sort="3" class="text-center text-nowrap">
- Options <span class="sort-arrow" aria-hidden="true"></span>
+ <span data-i18n="common.options">Options</span>
+ <span class="sort-arrow" aria-hidden="true"></span>
</th>
<th data-type="string" data-sortable="false" class="text-center text-nowrap">
- Actions
+ <span data-i18n="common.actions">Actions</span>
</th>
</tr>
</thead>
<!-- Loader -->
<div id="loader" class="text-center my-3" style="display: none;">
<div class="spinner-border text-primary" role="status">
- <span class="visually-hidden">Loading...</span>
+ <span class="visually-hidden" data-i18n="common.loading">Loading...</span>
</div>
</div>
- <div id="devices-container"></div>
<!-- Modals -->
<div id="modals-container"></div>
.table-responsive {
margin: 10px;
- max-height: calc(100dvh - 9rem);
+ max-height: calc(100dvh - 8.5rem);
}
.table {
<link rel="stylesheet" href="css/layout.css">
</head>
-<body>
+<body data-page-title="dashboard.title">
<!-- Topbar -->
<header class="topbar">
<div class="topbar-inner">
<line x1="12" y1="6" x2="12" y2="18" stroke="var(--accent)" stroke-width="2"></line>
<line x1="6" y1="12" x2="18" y2="12" stroke="var(--accent)" stroke-width="2"></line>
</svg>
- <span>Network Manager</span>
+ <span data-i18n="app.name">Network Manager</span>
</a>
<nav class="navbar">
<!-- Desktop nav -->
<div class="d-none d-md-flex gap-2 ms-auto">
- <a href="/hosts" class="btn btn-primary"> Hosts</a>
- <a href="/aliases" class="btn btn-primary"> Alias</a>
- <a href="/leases" class="btn btn-primary"> DHCP Leases</a>
- <a href="/devices" class="btn btn-primary active">Devices</a>
- <a href="/logs" class="btn btn-primary"> Logs</a>
- <a href="/settings" class="btn btn-primary"> Settings</a>
- <button class="btn btn-primary btn-logout">Logout</button>
+ <a href="/hosts" class="btn btn-primary" data-i18n="hosts.title">Hosts</a>
+ <a href="/aliases" class="btn btn-primary" data-i18n="aliases.title">Alias</a>
+ <a href="/leases" class="btn btn-primary" data-i18n="dhcp.title">DHCP Leases</a>
+ <a href="/devices" class="btn btn-primary active" data-i18n="devices.title">Devices</a>
+ <a href="/logs" class="btn btn-primary" data-i18n="logs.title">Logs</a>
+ <a href="/settings" class="btn btn-primary" data-i18n="settings.title">Settings</a>
+ <button class="btn btn-primary btn-logout" data-i18n="auth.logout">Logout</button>
</div>
</nav>
<div class="offcanvas offcanvas-end d-md-none" tabindex="-1" id="mobileMenu">
<div class="offcanvas-header">
- <h5 class="offcanvas-title">Network Manager</h5>
+ <h5 class="offcanvas-title" data-i18n="app.name">Network Manager</h5>
<button
type="button"
class="btn-close"
+ data-i18n-title="common.close"
+ data-i18n-aria-label="common.close"
data-bs-dismiss="offcanvas">
</button>
</div>
<div class="offcanvas-body">
<div class="d-grid gap-2">
- <a href="/hosts" class="btn btn-primary"> Hosts</a>
- <a href="/aliases" class="btn btn-primary"> Alias</a>
- <a href="/leases" class="btn btn-primary"> DHCP Leases</a>
- <a href="/devices" class="btn btn-primary active">Devices</a>
- <a href="/logs" class="btn btn-primary"> Logs</a>
- <a href="/settings" class="btn btn-primary"> Settings</a>
- <button class="btn btn-primary btn-logout">Logout</button>
+ <a href="/hosts" class="btn btn-primary" data-i18n="hosts.title">Hosts</a>
+ <a href="/aliases" class="btn btn-primary" data-i18n="aliases.title">Alias</a>
+ <a href="/leases" class="btn btn-primary" data-i18n="dhcp.title">DHCP Leases</a>
+ <a href="/devices" class="btn btn-primary active" data-i18n="devices.title">Devices</a>
+ <a href="/logs" class="btn btn-primary" data-i18n="logs.title">Logs</a>
+ <a href="/settings" class="btn btn-primary" data-i18n="settings.title">Settings</a>
+ <button class="btn btn-primary btn-logout" data-i18n="auth.logout">Logout</button>
</div>
</div>
</div>
<div class="col-auto col-md-auto">
<h2 class="mb-0 d-flex align-items-center gap-2 lh-1">
<span class="title-icon"><i class="bi bi-diagram-3-fill"></i></span>
- <span class="section-title">Devices List</span>
+ <span class="section-title" data-i18n="devices.page.title">Devices List</span>
</h2>
</div>
<input
type="text"
id="searchInput"
- placeholder="Search..."
class="form-control form-control-sm placeholder-italic"
- aria-label="Search device">
+ data-i18n-placeholder="common.search.placeholder"
+ data-i18n-aria-label="devices.search">
</div>
</div>
<!-- Add host Desktop -->
<button
class="btn btn-primary d-none d-md-flex align-items-center gap-2 px-3"
- title="Add Host"
- aria-label="Add Host"
+ data-i18n-title="hosts.add"
+ data-i18n-aria-label="hosts.add"
data-bs-toggle="modal" data-bs-target="#addHostModal">
<i class="bi bi-plus-lg"></i>
- <span class="label">Add Host</span>
+ <span class="label" data-i18n="hosts.add">Add Host</span>
</button>
<!-- Separator -->
<!-- Reload DNS Desktop -->
<button
class="btn btn-primary d-none d-md-flex align-items-center gap-2 px-3"
- title="Reload DNS (BIND)"
- aria-label="Reload DNS"
+ data-i18n-title="dns.reload.description"
+ data-i18n-aria-label="dns.reload.description"
data-action="reloadDns">
<i class="bi bi-arrow-repeat"></i>
- <span class="label">Reload DNS</span>
+ <span class="label" data-i18n="dns.reload">Reload DNS</span>
</button>
<!-- Reload DHCP Desktop -->
<button
class="btn btn-primary d-none d-md-flex align-items-center gap-2 px-3"
- title="Reload DHCP (Kea)"
- aria-label="Reload DHCP"
+ data-i18n-title="dhcp.reload.description"
+ data-i18n-aria-label="dhcp.reload.description"
data-action="reloadDhcp">
<i class="bi bi-arrow-repeat"></i>
- <span class="label">Reload DHCP</span>
+ <span class="label" data-i18n="dhcp.reload">Reload DHCP</span>
</button>
<!-- Mobile Dropdown -->
<li class="px-2 py-1">
<button
class="btn btn-primary w-100 d-flex align-items-center gap-2"
- title="Add Host"
- aria-label="Add Host"
+ data-i18n-title="hosts.add"
+ data-i18n-aria-label="hosts.add"
data-bs-toggle="modal" data-bs-target="#addHostModal">
<i class="bi bi-plus-lg"></i>
- <span>Add Host</span>
+ <span data-i18n="hosts.add">Add Host</span>
</button>
</li>
<li class="px-2 py-1">
<button
class="btn btn-primary w-100 d-flex align-items-center gap-2"
- title="Reload DNS (BIND)"
- aria-label="Reload DNS"
+ data-i18n-title="dns.reload.description"
+ data-i18n-aria-label="dns.reload.description"
data-action="reloadDns">
<i class="bi bi-arrow-repeat"></i>
- <span>Reload DNS</span>
+ <span data-i18n="dns.reload">Reload DNS</span>
</button>
</li>
<li class="px-2 py-1">
<button
class="btn btn-primary w-100 d-flex align-items-center gap-2"
- title="Reload DHCP (Kea)"
- aria-label="Reload DHCP"
+ data-i18n-title="dhcp.reload.description"
+ data-i18n-aria-label="dhcp.reload.description"
data-action="reloadDhcp">
<i class="bi bi-arrow-repeat"></i>
- <span>Reload DHCP</span>
+ <span data-i18n="dhcp.reload">Reload DHCP</span>
</button>
</li>
</ul>
<input
type="text"
id="searchInputMobile"
- placeholder="Search..."
class="form-control form-control-sm placeholder-italic"
- aria-label="Search device">
+ data-i18n-placeholder="common.search.placeholder"
+ data-i18n-aria-label="devices.search">
</div>
</div>
</section>
<thead class="table-light">
<tr>
<th data-type="ipv4" data-sortable="true" data-sort="0">
- IP Address <span class="sort-arrow" aria-hidden="true"></span>
+ <span data-i18n="hosts.ipv4">IP Address</span>
+ <span class="sort-arrow" aria-hidden="true"></span>
</th>
<th data-type="mac" data-sortable="true" data-sort="1">
- MAC Address <span class="sort-arrow" aria-hidden="true"></span>
+ <span data-i18n="hosts.mac">MAC Address</span>
+ <span class="sort-arrow" aria-hidden="true"></span>
</th>
<th data-type="string" data-sortable="true" data-sort="2">
- Hostname <span class="sort-arrow" aria-hidden="true"></span>
+ <span data-i18n="hosts.hostname">Hostname</span>
+ <span class="sort-arrow" aria-hidden="true"></span>
</th>
<th data-type="string" data-sortable="true" data-sort="3">
- Description <span class="sort-arrow" aria-hidden="true"></span>
+ <span data-i18n="common.description">Description</span>
+ <span class="sort-arrow" aria-hidden="true"></span>
</th>
<th data-type="string" data-sortable="true" data-sort="4" class="text-center text-nowrap">
- State <span class="sort-arrow" aria-hidden="true"></span>
+ <span data-i18n="common.state">State</span>
+ <span class="sort-arrow" aria-hidden="true"></span>
</th>
<th data-type="string" data-sortable="true" data-sort="5" class="text-center text-nowrap">
- Active <span class="sort-arrow" aria-hidden="true"></span>
+ <span data-i18n="devices.active">Active</span>
+ <span class="sort-arrow" aria-hidden="true"></span>
</th>
<th data-type="string" data-sortable="false" class="text-center text-nowrap">
- Actions
+ <span data-i18n="common.actions">Actions</span>
</th>
</tr>
</thead>
<!-- Loader -->
<div id="loader" class="text-center my-3" style="display: none;">
<div class="spinner-border text-primary" role="status">
- <span class="visually-hidden">Loading...</span>
+ <span class="visually-hidden" data-i18n="common.loading">Loading...</span>
</div>
</div>
- <div id="devices-container"></div>
<!-- Modals -->
<div id="modals-container"></div>
<link rel="stylesheet" href="css/layout.css">
</head>
-<body>
+<body data-page-title="dashboard.title">
<!-- Topbar -->
<header class="topbar">
<div class="topbar-inner">
<line x1="12" y1="6" x2="12" y2="18" stroke="var(--accent)" stroke-width="2"></line>
<line x1="6" y1="12" x2="18" y2="12" stroke="var(--accent)" stroke-width="2"></line>
</svg>
- <span>Network Manager</span>
+ <span data-i18n="app.name">Network Manager</span>
</a>
<nav class="navbar">
<!-- Desktop nav -->
<div class="d-none d-md-flex gap-2 ms-auto">
- <a href="/hosts" class="btn btn-primary active">Hosts</a>
- <a href="/aliases" class="btn btn-primary"> Alias</a>
- <a href="/leases" class="btn btn-primary"> DHCP Leases</a>
- <a href="/devices" class="btn btn-primary"> Devices</a>
- <a href="/logs" class="btn btn-primary"> Logs</a>
- <a href="/settings" class="btn btn-primary"> Settings</a>
- <button class="btn btn-primary btn-logout">Logout</button>
+ <a href="/hosts" class="btn btn-primary active" data-i18n="hosts.title">Hosts</a>
+ <a href="/aliases" class="btn btn-primary" data-i18n="aliases.title">Alias</a>
+ <a href="/leases" class="btn btn-primary" data-i18n="dhcp.title">DHCP Leases</a>
+ <a href="/devices" class="btn btn-primary" data-i18n="devices.title">Devices</a>
+ <a href="/logs" class="btn btn-primary" data-i18n="logs.title">Logs</a>
+ <a href="/settings" class="btn btn-primary" data-i18n="settings.title">Settings</a>
+ <button class="btn btn-primary btn-logout" data-i18n="auth.logout">Logout</button>
</div>
</nav>
<div class="offcanvas offcanvas-end d-md-none" tabindex="-1" id="mobileMenu">
<div class="offcanvas-header">
- <h5 class="offcanvas-title">Network Manager</h5>
+ <h5 class="offcanvas-title" data-i18n="app.name">Network Manager</h5>
<button
type="button"
class="btn-close"
+ data-i18n-title="common.close"
+ data-i18n-aria-label="common.close"
data-bs-dismiss="offcanvas">
</button>
</div>
<div class="offcanvas-body">
<div class="d-grid gap-2">
- <a href="/hosts" class="btn btn-primary active">Hosts</a>
- <a href="/aliases" class="btn btn-primary"> Alias</a>
- <a href="/leases" class="btn btn-primary"> DHCP Leases</a>
- <a href="/devices" class="btn btn-primary"> Devices</a>
- <a href="/logs" class="btn btn-primary"> Logs</a>
- <a href="/settings" class="btn btn-primary"> Settings</a>
- <button class="btn btn-primary btn-logout">Logout</button>
+ <a href="/hosts" class="btn btn-primary active" data-i18n="hosts.title">Hosts</a>
+ <a href="/aliases" class="btn btn-primary" data-i18n="aliases.title">Alias</a>
+ <a href="/leases" class="btn btn-primary" data-i18n="dhcp.title">DHCP Leases</a>
+ <a href="/devices" class="btn btn-primary" data-i18n="devices.title">Devices</a>
+ <a href="/logs" class="btn btn-primary" data-i18n="logs.title">Logs</a>
+ <a href="/settings" class="btn btn-primary" data-i18n="settings.title">Settings</a>
+ <button class="btn btn-primary btn-logout" data-i18n="auth.logout">Logout</button>
</div>
</div>
</div>
<div class="col-auto col-md-auto">
<h2 class="mb-0 d-flex align-items-center gap-2 lh-1">
<span class="title-icon"><i class="bi bi-hdd-network"></i></span>
- <span class="section-title">Hosts List</span>
+ <span class="section-title" data-i18n="hosts.page.title">Hosts List</span>
</h2>
</div>
<input
type="text"
id="searchInput"
- placeholder="Search..."
class="form-control form-control-sm placeholder-italic"
- aria-label="Search hosts">
+ data-i18n-placeholder="common.search.placeholder"
+ data-i18n-aria-label="hosts.search">
</div>
</div>
<!-- Add host Desktop -->
<button
class="btn btn-primary d-none d-md-flex align-items-center gap-2 px-3"
- title="Add Host"
- aria-label="Add Host"
+ data-i18n-title="hosts.add"
+ data-i18n-aria-label="hosts.add"
data-bs-toggle="modal" data-bs-target="#addHostModal">
<i class="bi bi-plus-lg"></i>
- <span class="label">Add Host</span>
+ <span class="label" data-i18n="hosts.add">Add Host</span>
</button>
<!-- Separator -->
<!-- Reload DNS Desktop -->
<button
class="btn btn-primary d-none d-md-flex align-items-center gap-2 px-3"
- title="Reload DNS (BIND)"
- aria-label="Reload DNS"
+ data-i18n-title="dns.reload.description"
+ data-i18n-aria-label="dns.reload.description"
data-action="reloadDns">
<i class="bi bi-arrow-repeat"></i>
- <span class="label">Reload DNS</span>
+ <span class="label" data-i18n="dns.reload">Reload DNS</span>
</button>
<!-- Reload DHCP Desktop -->
<button
class="btn btn-primary d-none d-md-flex align-items-center gap-2 px-3"
- title="Reload DHCP (Kea)"
- aria-label="Reload DHCP"
+ data-i18n-title="dhcp.reload.description"
+ data-i18n-aria-label="dhcp.reload.description"
data-action="reloadDhcp">
<i class="bi bi-arrow-repeat"></i>
- <span class="label">Reload DHCP</span>
+ <span class="label" data-i18n="dhcp.reload">Reload DHCP</span>
</button>
<!-- Mobile Dropdown -->
<li class="px-2 py-1">
<button
class="btn btn-primary w-100 d-flex align-items-center gap-2"
- title="Add Host"
- aria-label="Add Host"
+ data-i18n-title="hosts.add"
+ data-i18n-aria-label="hosts.add"
data-bs-toggle="modal" data-bs-target="#addHostModal">
<i class="bi bi-plus-lg"></i>
- <span>Add Host</span>
+ <span data-i18n="hosts.add">Add Host</span>
</button>
</li>
<li class="px-2 py-1">
<button
class="btn btn-primary w-100 d-flex align-items-center gap-2"
- title="Reload DNS (BIND)"
- aria-label="Reload DNS"
+ data-i18n-title="dns.reload.description"
+ data-i18n-aria-label="dns.reload.description"
data-action="reloadDns">
<i class="bi bi-arrow-repeat"></i>
- <span>Reload DNS</span>
+ <span data-i18n="dns.reload">Reload DNS</span>
</button>
</li>
<li class="px-2 py-1">
<button
class="btn btn-primary w-100 d-flex align-items-center gap-2"
- title="Reload DHCP (Kea)"
- aria-label="Reload DHCP"
+ data-i18n-title="dhcp.reload.description"
+ data-i18n-aria-label="dhcp.reload.description"
data-action="reloadDhcp">
<i class="bi bi-arrow-repeat"></i>
- <span>Reload DHCP</span>
+ <span data-i18n="dhcp.reload">Reload DHCP</span>
</button>
</li>
</ul>
<input
type="text"
id="searchInputMobile"
- placeholder="Search..."
class="form-control form-control-sm placeholder-italic"
- aria-label="Search hosts">
+ data-i18n-placeholder="common.search.placeholder"
+ data-i18n-aria-label="hosts.search">
</div>
</div>
</section>
<thead class="table-light">
<tr>
<th data-type="string" data-sortable="true" data-sort="0">
- Hostname <span class="sort-arrow" aria-hidden="true"></span>
+ <span data-i18n="hosts.hostname">Hostname</span>
+ <span class="sort-arrow" aria-hidden="true"></span>
</th>
<th data-type="ipv4" data-sortable="true" data-sort="1">
- IP Address <span class="sort-arrow" aria-hidden="true"></span>
+ <span data-i18n="hosts.ipv4">IP Address</span>
+ <span class="sort-arrow" aria-hidden="true"></span>
</th>
<th data-type="mac" data-sortable="true" data-sort="2">
- MAC Address <span class="sort-arrow" aria-hidden="true"></span>
+ <span data-i18n="hosts.mac">MAC Address</span>
+ <span class="sort-arrow" aria-hidden="true"></span>
</th>
<th data-type="string" data-sortable="true" data-sort="3">
- Description <span class="sort-arrow" aria-hidden="true"></span>
+ <span data-i18n="common.description">Description</span>
+ <span class="sort-arrow" aria-hidden="true"></span>
</th>
<th data-type="string" data-sortable="true" data-sort="4" class="text-center text-nowrap">
- Options <span class="sort-arrow" aria-hidden="true"></span>
+ <span data-i18n="common.options">Options</span>
+ <span class="sort-arrow" aria-hidden="true"></span>
</th>
<th data-type="string" data-sortable="false" class="text-center text-nowrap">
- Actions
+ <span data-i18n="common.actions">Actions</span>
</th>
</tr>
</thead>
<!-- Loader -->
<div id="loader" class="text-center my-3" style="display: none;">
<div class="spinner-border text-primary" role="status">
- <span class="visually-hidden">Loading...</span>
+ <span class="visually-hidden" data-i18n="common.loading">Loading...</span>
</div>
</div>
- <div id="devices-container"></div>
<!-- Modals -->
<div id="modals-container"></div>
<link rel="stylesheet" href="css/layout.css">
</head>
-<body>
+<body data-page-title="dashboard.title">
<!-- Topbar -->
<header class="topbar">
<div class="topbar-inner">
<line x1="12" y1="6" x2="12" y2="18" stroke="var(--accent)" stroke-width="2"></line>
<line x1="6" y1="12" x2="18" y2="12" stroke="var(--accent)" stroke-width="2"></line>
</svg>
- <span>Network Manager</span>
+ <span data-i18n="app.name">Network Manager</span>
</a>
<nav class="navbar">
<!-- Desktop nav -->
<div class="d-none d-md-flex gap-2 ms-auto">
- <button class="btn btn-outline-primary btn-api" title="API Status" aria-label="API Status" data-action="apiCheck">API Status</button>
- <button class="btn btn-primary btn-logout">Logout</button>
+ <button class="btn btn-outline-primary btn-api" data-i18n-title="header.api_status" data-i18n-aria-label="header.api_status" data-action="apiCheck" data-i18n="header.api_status">API Status</button>
+ <button class="btn btn-primary btn-logout" data-i18n="auth.logout">Logout</button>
</div>
</nav>
<div class="offcanvas offcanvas-end d-md-none" tabindex="-1" id="mobileMenu">
<div class="offcanvas-header">
- <h5 class="offcanvas-title">Network Manager</h5>
+ <h5 class="offcanvas-title" data-i18n="app.name">Network Manager</h5>
<button
type="button"
class="btn-close"
+ data-i18n-title="common.close"
+ data-i18n-aria-label="common.close"
data-bs-dismiss="offcanvas">
</button>
</div>
<div class="offcanvas-body">
<div class="d-grid gap-2">
- <button class="btn btn-outline-primary btn-api" title="API Status" aria-label="API Status" data-action="apiCheck">API Status</button>
- <button class="btn btn-primary btn-logout">Logout</button>
+ <button class="btn btn-outline-primary btn-api" data-i18n-title="header.api_status" data-i18n-aria-label="header.api_status" data-action="apiCheck" data-i18n="header.api_status">API Status</button>
+ <button class="btn btn-primary btn-logout" data-i18n="auth.logout">Logout</button>
</div>
</div>
</div>
<div class="col-auto col-md-auto">
<h2 class="mb-0 d-flex align-items-center gap-2 lh-1">
<span class="title-icon"><i class="bi bi-grid me-2"></i></span>
- <span class="section-title">Dashboard</span>
+ <span class="section-title" data-i18n="dashboard.title">Dashboard</span>
</h2>
</div>
</div>
<!-- DNS -->
<div class="tile">
<div class="tile-icon"><i class="bi bi-diagram-3"></i></div>
- <h3>DNS (BIND)</h3>
- <p>Zone, record e configurazioni.</p>
+ <h3 data-i18n="dns.card.title">DNS (BIND)</h3>
+ <p data-i18n="dns.card.description">Zones, records and configuration.</p>
<div class="mt-2 d-flex gap-2 flex-wrap">
- <a href="/dns-config" class="btn btn-primary btn-sm" title="DNS Configuration" aria-label="DNS Configuration">
- <i class="bi bi-list-ul me-1"></i><span class="label">DNS Configuration</span>
+ <a href="/dns-config" class="btn btn-primary btn-sm" data-i18n-title="dns.config" data-i18n-aria-label="dns.config">
+ <i class="bi bi-list-ul me-1"></i><span class="label" data-i18n="dns.config">DNS Configuration</span>
</a>
- <button class="btn btn-primary btn-sm" title="Reload DNS (BIND)" aria-label="Reload DNS" data-action="reloadDns">
- <i class="bi bi-arrow-repeat me-1"></i><span class="label">Reload DNS</span>
+ <button class="btn btn-primary btn-sm" data-i18n-title="dns.reload" data-i18n-aria-label="dns.reload" data-action="reloadDns">
+ <i class="bi bi-arrow-repeat me-1"></i><span class="label" data-i18n="dns.reload">Reload DNS</span>
</button>
</div>
</div>
<!-- DHCP -->
<div class="tile">
<div class="tile-icon"><i class="bi bi-broadcast"></i></div>
- <h3>DHCP (Kea)</h3>
- <p>Pools, leases, reservations.</p>
+ <h3 data-i18n="dhcp.card.title">DHCP (Kea)</h3>
+ <p data-i18n="dhcp.card.description">Pools, leases, reservations.</p>
<div class="mt-2 d-flex gap-2 flex-wrap">
- <a href="/leases" class="btn btn-primary btn-sm" title="DHCP Leases (Kea)" aria-label="DHCP Leases">
- <i class="bi bi-list-ul me-1"></i><span class="label">DHCP Leases</span>
+ <a href="/leases" class="btn btn-primary btn-sm" data-i18n-title="dhcp.leases" data-i18n-aria-label="dhcp.leases">
+ <i class="bi bi-list-ul me-1"></i><span class="label" data-i18n="dhcp.leases">DHCP Leases</span>
</a>
- <button class="btn btn-primary btn-sm" title="Reload DHCP (Kea)" aria-label="Reload DHCP" data-action="reloadDhcp">
- <i class="bi bi-arrow-repeat me-1"></i><span class="label">Reload DHCP</span>
+ <button class="btn btn-primary btn-sm" data-i18n-title="dns.reload" data-i18n-aria-label="dhcp.reload" data-action="reloadDhcp">
+ <i class="bi bi-arrow-repeat me-1"></i><span class="label" data-i18n="dhcp.reload">Reload DHCP</span>
</button>
</div>
</div>
<!-- Hosts -->
<a href="/hosts" class="tile text-decoration-none">
<div class="tile-icon"><i class="bi bi-hdd-network"></i></div>
- <h3>Hosts</h3>
- <p>Inventario IP/MAC.</p>
+ <h3 data-i18n="hosts.card.title">Hosts</h3>
+ <p data-i18n="hosts.card.description">IP/MAC inventory.</p>
</a>
<!-- Aliases -->
<a href="/aliases" class="tile text-decoration-none">
<div class="tile-icon"><i class="bi bi-diagram-2"></i></div>
- <h3>Aliases</h3>
- <p>Inventario alias DNS.</p>
+ <h3 data-i18n="aliases.card.title">Aliases</h3>
+ <p data-i18n="aliases.card.description">DNS alias inventory.</p>
</a>
<!-- Devices Status -->
<a href="/devices" class="tile text-decoration-none">
<div class="tile-icon"><i class="bi bi-diagram-3-fill"></i></div>
- <h3>Devices</h3>
- <p>Device status (Host + DHCP).</p>
+ <h3 data-i18n="devices.card.title">Devices</h3>
+ <p data-i18n="devices.card.description">Device status (Host + DHCP).</p>
</a>
<!-- Certificati -->
<a href="/api/certificates" class="tile text-decoration-none">
<div class="tile-icon"><i class="bi bi-shield-lock"></i></div>
- <h3>Certificati</h3>
- <p>Let’s Encrypt e rinnovi.</p>
+ <h3 data-i18n="certificates.card.title">Certificates</h3>
+ <p data-i18n="certificates.card.description">Let's Encrypt certificates and renewals.</p>
</a>
<!-- Backup & Restore -->
<div class="tile" data-action="openBackupModal" role="button">
<div class="tile-icon"><i class="bi bi-arrow-counterclockwise"></i></div>
- <h3>Backup & Restore</h3>
- <p>Esecuzione backup e gestione archivi.</p>
+ <h3 data-i18n="backup.card.title">Backup & Restore</h3>
+ <p data-i18n="backup.card.description">Backup execution and archive management.</p>
<div class="mt-2 d-flex gap-2 flex-wrap">
- <button class="btn btn-primary btn-sm" title="Start Backup" aria-label="Start Backup" data-action="startBackup">
- <i class="bi bi-cloud-upload me-1"></i><span class="label">Create Backup</span>
+ <button class="btn btn-primary btn-sm" data-i18n-title="backup.create" data-i18n-aria-label="backup.create" data-action="startBackup">
+ <i class="bi bi-cloud-upload me-1"></i><span class="label" data-i18n="backup.create">Create Backup</span>
</button>
- <button class="btn btn-primary btn-sm" title="Backup Management" aria-label="Backup Management" data-action="openBackupModal">
- <i class="bi bi-cloud-download me-1"></i><span class="label">Backup Management</span>
+ <button class="btn btn-primary btn-sm" data-i18n-title="backup.manage" data-i18n-aria-label="backup.manage" data-action="openBackupModal">
+ <i class="bi bi-cloud-download me-1"></i><span class="label" data-i18n="backup.manage">Backup Management</span>
</button>
</div>
</div>
<!-- Logs -->
<a href="/logs" class="tile text-decoration-none">
<div class="tile-icon"><i class="bi bi-file-earmark-text"></i></div>
- <h3>Logs</h3>
- <p>Eventi e access log.</p>
+ <h3 data-i18n="logs.card.title">Logs</h3>
+ <p data-i18n="logs.card.description">Events and access logs.</p>
</a>
<!-- Settings -->
<a href="/settings" class="tile text-decoration-none">
<div class="tile-icon"><i class="bi bi-gear"></i></div>
- <h3>Impostazioni</h3>
- <p>Configurazione sistema e variabili.</p>
+ <h3 data-i18n="settings.card.title">Settings</h3>
+ <p data-i18n="settings.card.description">System configuration and variables.</p>
</a>
<!-- Health -->
<a href="/api/health" id="healthBtn" class="tile text-decoration-none" data-action="openHealthModal" aria-controls="healthModal" role="button">
<div class="tile-icon"><i class="bi bi-heart-pulse"></i></div>
- <h3>Health</h3>
- <p>Stato servizi e risorse.</p>
+ <h3 data-i18n="health.card.title">Health</h3>
+ <p data-i18n="health.card.description">Service and resource status.</p>
</a>
</div>
<div id="modals-container"></div>
<!-- Scripts -->
- <script type=module src="js/index.js"></script>
+ <script type="module" src="js/index.js"></script>
<script type="module" src="js/session.js"></script>
<!-- Bootstrap JS -->
-// Import common js
+// -------------------------------------------------------
+// IMPORT
+// -------------------------------------------------------
import { loadModals, showToast, sortTable, initSortableTable, resetSorting, handleSearch, filterTable, clearSearch, handleReload } from './common.js';
-// Import services
import { serviceReloadDNS, serviceReloadDHCP, serviceGetAliases, serviceGetAlias, serviceCreateAlias, serviceUpdateAlias, serviceDeleteAlias } from './services.js';
+import { loadLanguage, translatePage, t } from "./i18n.js";
+import { getBackendMessage } from "./backendMessages.js";
// -----------------------------
// State variables
viewAliases = [...allAliases];
} catch (err) {
- console.error(err?.message || "Error loading aliases");
- showToast(err?.message || "Error loading aliase", false);
+ console.error(err?.message || t("aliases.list.error"));
+ showToast(err?.message || t("aliases.list.error"), false);
allAliases = [];
viewAliases = [];
// hide loader and show table
const trEmpty = document.createElement("tr");
const tdEmpty = document.createElement("td");
tdEmpty.colSpan = 5;
- tdEmpty.textContent = "No alias available.";
+ tdEmpty.textContent = t("aliases.empty");
tdEmpty.style.textAlign = "center";
trEmpty.appendChild(tdEmpty);
tbody.appendChild(trEmpty);
// fragment per performance
const frag = document.createDocumentFragment();
- viewAliases.forEach(h => {
+ viewAliases.forEach(a => {
- const id = Number(h.id);
+ const id = Number(a.id);
const tr = document.createElement("tr");
// Name
{
const td = document.createElement("td");
- const val = (h.name ?? "").toString();
+ const val = (a.name ?? "").toString();
td.textContent = val;
if (val) td.setAttribute("data-value", val.toLowerCase());
tr.appendChild(td);
// Target
{
const td = document.createElement("td");
- const val = (h.target ?? "").toString();
+ const val = (a.target ?? "").toString();
td.textContent = val;
if (val) td.setAttribute("data-value", val.toLowerCase());
tr.appendChild(td);
// Description
{
const td = document.createElement("td");
- const val = (h.description ?? "").toString();
+ const val = (a.description ?? "").toString();
td.textContent = val;
if (val) td.setAttribute("data-value", val.toLowerCase());
tr.appendChild(td);
//
// SSL icon
//
- const sslEnabled = !!h.ssl_enabled;
- td.setAttribute("data-value", sslEnabled ? "true" : "false");
- td.setAttribute("aria-label", sslEnabled ? "SSL attivo" : "SSL non attivo");
- const icon = document.createElement("i");
+ const sslEnabled = !!a.ssl_enabled;
+ const wrapperSslIcon = document.createElement("span");
+ const sslIcon = document.createElement("i");
+ let description = "";
if (sslEnabled) {
- icon.className = "bi bi-shield-lock-fill icon icon-static";
- icon.setAttribute("aria-hidden", "true");
- icon.setAttribute("title", "SSL certificate enabled");
+ sslIcon.className = "bi bi-shield-lock-fill icon icon-static";
+ description = t("aliases.ssl.enabled");
+ wrapperSslIcon.setAttribute("title", description);
+ wrapperSslIcon.setAttribute("aria-label", description);
} else {
- icon.className = "bi bi-shield-lock-fill icon icon-static icon-placeholder";
- icon.setAttribute("aria-hidden", "true");
+ sslIcon.className = "bi bi-shield-lock-fill icon icon-static icon-placeholder";
+ sslIcon.setAttribute("aria-hidden", "true");
}
- td.appendChild(icon);
+ wrapperSslIcon.appendChild(sslIcon);
+ td.appendChild(wrapperSslIcon);
//
// visibility icon
//
- const ext = (h.visibility ?? "").toString();
- let aria = "";
+ const ext = (a.visibility ?? "").toString();
+ const wrapperVisibilityIcon = document.createElement("span");
+ const visibilityIcon = document.createElement("i");
let iconClass = "";
switch (ext) {
case "0":
// Only local (CNAME record internally resolved)
- aria = "Only local (CNAME record internally resolved)";
+ description = t("aliases.visibility.local.description");
iconClass = "bi bi-hdd-network";
break;
case "1":
// Local and external (CNAME record internally resolved, CNAME externally)
- aria = "Internal and external are identical";
+ description = t("aliases.visibility.global.description");
iconClass = "bi bi-globe2";
break;
case "2":
// CNAME -> DDNS / external_name
- aria = "External is a CNAME to external_name";
+ description = t("aliases.visibility.alias.description");
iconClass = "bi bi-link-45deg";
break;
}
if (iconClass) {
- const icon = document.createElement("i");
- icon.className = iconClass + " icon icon-static";
- icon.setAttribute("aria-hidden", "true");
- icon.setAttribute("title", aria);
- td.appendChild(icon);
+ visibilityIcon.className = iconClass + " icon icon-static";
+ wrapperVisibilityIcon.setAttribute("title", description);
+ wrapperVisibilityIcon.setAttribute("aria-label", description);
+ wrapperVisibilityIcon.appendChild(visibilityIcon);
+ td.appendChild(wrapperVisibilityIcon);
}
-
+ if (ext) td.setAttribute("data-value", ext);
tr.appendChild(td);
}
// Edit Button
const editSpan = document.createElement("span");
+ const editText = t("aliases.edit");
editSpan.className = "action-icon";
editSpan.setAttribute("role", "button");
editSpan.tabIndex = 0;
- editSpan.title = "Edit alias";
- editSpan.setAttribute("aria-label", "Edit alias");
+ editSpan.title = editText;
+ editSpan.setAttribute("aria-label", editText);
editSpan.setAttribute("data-bs-toggle", "modal");
editSpan.setAttribute("data-bs-target", "#addAliasModal");
editSpan.setAttribute("data-action", "edit");
// Delete Button
const delSpan = document.createElement("span");
+ const deleteText = t("aliases.delete");
delSpan.className = "action-icon";
delSpan.setAttribute("role", "button");
delSpan.tabIndex = 0;
- delSpan.title = "Delete alias";
- delSpan.setAttribute("aria-label", "Delete alias");
+ delSpan.title = deleteText;
+ delSpan.setAttribute("aria-label", deleteText);
delSpan.setAttribute("data-action", "delete");
delSpan.setAttribute("data-alias-id", String(id));
{
document.getElementById('searchInput')?.value ||
document.getElementById('searchInputMobile')?.value;
if (term?.trim()) {
- handleSearch(term);
+ handleSearch(term, filterTable);
}
}
document.getElementById("aliasVisibilityLocal").checked = true;
}
} catch (err) {
- console.error(err?.message || "Error loading alias");
- showToast(err?.message || "Error loading alias", false);
+ console.error(err?.message || t("aliases.loaded.error"));
+ showToast(err?.message || t("aliases.loaded.error"), false);
}
}
async function saveAlias(aliasData) {
// Validate alias
if (!aliasData.name.trim()) {
- showToast("Alias is required", false);
+ showToast(t("validation.alias.required"), false);
return false;
}
// Validate Target
if (!aliasData.target.trim()) {
- showToast("Target is required", false);
+ showToast(t("validation.target.required"), false);
return false;
}
result = await serviceCreateAlias(aliasData);
}
- const msg = (typeof result === 'object' && result?.message)
- ? result.message
- : editingAliasId !== null
- ? 'Alias updated successfully'
- : 'Alias created successfully';
+ const msg = getBackendMessage(
+ result,
+ editingAliasId !== null
+ ? "aliases.updated.ok"
+ : "aliases.created.ok"
+ );
showToast(msg, true);
return true;
} catch (err) {
- console.error(err?.message || "Error saving alias");
- showToast(err?.message || "Error saving alias", false);
+
+ const msg = getBackendMessage(
+ err,
+ editingAliasId !== null
+ ? "aliases.updated.error"
+ : "aliases.created.error"
+ );
+
+ showToast(msg, false);
}
return false;
}
} catch (err) {
- console.error(err?.message || "Error saving alias");
- showToast(err?.message || "Error saving alias", false);
+ console.error(
+ err?.message ||
+ (editingAliasId !== null
+ ? t("aliases.updated.error")
+ : t("aliases.created.error"))
+ );
+
+ showToast(
+ err?.message ||
+ (editingAliasId !== null
+ ? t("aliases.updated.error")
+ : t("aliases.created.error")),
+ false
+ );
}
return false;
// Get alias ID
const id = Number(el.dataset.aliasId);
if (!Number.isFinite(id)) {
- showToast('Alias id not valid for delete', false);
+ showToast(t("aliases.delete.invalid_id"), false);
return;
}
try {
const result = await serviceDeleteAlias(id);
-
- const msg = (typeof result === 'object' && result?.message)
- ? result.message
- : 'Alias deleted successfully';
-
+ const msg = getBackendMessage(result, "aliases.deleted.ok");
showToast(msg, true);
// Reload aliases
return true;
} catch (err) {
- console.error(err?.message || "Error deleting alias");
- showToast(err?.message || "Error deleting alias", false);
+ console.error(err?.message || t("aliases.deleted.error"));
+ showToast(err?.message || t("aliases.deleted.error"), false);
}
return false;
await handleReload(
el,
serviceReloadDNS,
- "DNS reload successfully",
- "Error reloading DNS",
- "Reloading DNS..."
+ t("dns.reload.ok"),
+ t("dns.reload.error"),
+ t("dns.reload.progress")
);
},
// Reload DHCP
await handleReload(
el,
serviceReloadDHCP,
- "DHCP reload successfully",
- "Error reloading DHCP",
- "Reloading DHCP..."
+ t("dhcp.reload.ok"),
+ t("dhcp.reload.error"),
+ t("dhcp.reload.progress")
);
},
};
// -----------------------------
async function initApp() {
+ // Loading translation
+ try {
+ await loadLanguage();
+ } catch (err) {
+ console.error(err?.message || t("app.translation.error"));
+ showToast(t("app.translation.error"), false);
+ }
+
// Load modals (Bootstrap 5 requires JS initialization for dynamic content)
try {
await loadModals();
} catch (err) {
- console.error(err?.message || "Error loading modals");
- showToast(err?.message || "Error loading modals", false);
+ console.error(err?.message || t("app.modals.error"));
+ showToast(t("app.modals.error"), false);
}
+ // Translate page
+ translatePage();
+
// Load data (aliases)
try {
await fetchAliases();
updateTable();
} catch (err) {
- console.error(err?.message || "Error loading aliases");
- showToast(err?.message || "Error loading aliases:", false);
+ console.error(err?.message || t("aliases.list.error"));
+ showToast(err?.message || t("aliases.list.error"), false);
}
initUI();
// live filter
input.addEventListener("input", (e) => {
- handleSearch(e.target.value);
+ handleSearch(e.target.value, filterTable);
});
});
}
try {
await editAlias(id);
} catch (err) {
- showToast(err?.message || "Error loading alias", false);
+ showToast(err?.message || t("aliases.loaded.error"), false);
// Close modal
modalEl.addEventListener('shown.bs.modal', () => {
closeAddAliasModal();
try {
await handler(e, el);
} catch (err) {
- console.error(err?.message || 'Action error');
- showToast(err?.message || 'Action error', false);
+ console.error(err?.message || t("app.action.error"));
+ showToast(err?.message || t("app.action.error"), false);
}
}
// Handle HTTP error
if (!res.ok) {
- const serverMsg =
- data?.detail?.message?.trim()
- || (typeof data?.detail === 'string' ? data.detail.trim() : '')
- || data?.message?.trim()
- || data?.error?.message?.trim()
- || (typeof data?.error === 'string' ? data.error.trim() : '');
+
+ const detail = data?.detail ?? data;
const err = new Error(
- `${errorPrefix}${serverMsg ? `: ${serverMsg}` : ''}`
+ detail?.message || errorPrefix
);
+
err.status = res.status;
+ err.code = detail?.code;
+ err.details = detail?.details;
+
throw err;
}
--- /dev/null
+// -------------------------------------------------------
+// IMPORT
+// -------------------------------------------------------
+import { t } from "./i18n.js";
+
+const CODE_MAP = {
+ // APP
+ APP_RESTARTING: "app.restart.progress",
+
+ // Hosts
+ HOSTS_GET_ERROR: "hosts.list.error",
+ HOST_GET_ERROR: "hosts.loaded.error",
+ HOST_ADDED: "hosts.created.ok",
+ HOST_ADD_ERROR: "hosts.created.error",
+ HOST_UPDATED: "hosts.updated.ok",
+ HOST_UPDATE_ERROR: "hosts.updated.error",
+ HOST_DELETED: "hosts.deleted.ok",
+ HOST_DELETE_ERROR: "hosts.deleted.error",
+ HOST_NOT_FOUND: "hosts.not.found",
+ HOST_ALREADY_PRESENT: "hosts.already.present",
+
+ // Aliases
+ ALIASES_GET_ERROR: "aliases.list.error",
+ ALIAS_GET_ERROR: "aliases.loaded.error",
+ ALIAS_ADDED: "aliases.created.ok",
+ ALIAS_ADD_ERROR: "aliases.created.error",
+ ALIAS_UPDATED: "aliases.updated.ok",
+ ALIAS_UPDATE_ERROR: "aliases.updated.error",
+ ALIAS_DELETED: "aliases.deleted.ok",
+ ALIAS_DELETE_ERROR: "aliases.deleted.error",
+ ALIAS_NOT_FOUND: "aliases.not.found",
+ ALIAS_ALREADY_PRESENT: "aliases.already.present",
+
+ // DNS
+ DNS_RELOAD_OK: "dns.reload.ok",
+ DNS_RELOAD_ERROR: "dns.reload.error",
+
+ // DHCP
+ DHCP_RELOAD_OK: "dhcp.reload.ok",
+ DHCP_RELOAD_ERROR: "dhcp.reload.error",
+ DHCP_LEASES_ERROR: "dhcp.leases.list.error",
+ DHCP_LEASE_ERROR: "dhcp.leases.loaded.error",
+ DHCP_LEASE_DELETED: "dhcp.leases.deleted.ok",
+ DHCP_LEASE_DELETE_ERROR: "dhcp.leases.deleted.error",
+ DHCP_LEASE_NOT_FOUND: "dhcp.leases.not.found",
+
+ BACKUP_CREATED: "backup.create_ok",
+ BACKUP_CREATE_PARTIAL: "backup.create_partial",
+ BACKUP_CREATE_ERROR: "backup.create_error",
+ BACKUP_RESTORED: "backup.restore_ok",
+ BACKUP_RESTORE_PARTIAL: "backup.restore_partial",
+ BACKUP_RESTORE_ERROR: "backup.restore_error",
+ BACKUP_DELETED: "backup.delete_ok",
+ BACKUP_DELETE_ERROR: "backup.delete_error",
+ BACKUP_NOT_FOUND: "backup.not.found",
+
+ // Settings
+ CONFIGS_GET_ERROR: "settings.list.error",
+ CONFIG_GET_ERROR: "settings.loaded.error",
+ CONFIG_UPDATED: "settings.updated.ok",
+ CONFIG_UPDATE_ERROR: "settings.updated.error",
+ CONFIG_RESTORED: "settings.restored.ok",
+ CONFIG_RESTORED_ERROR: "settings.restored.error",
+ CONFIG_NOT_FOUND: "settings.not.found",
+};
+
+export function getBackendMessage(result, fallbackKey = null) {
+
+ const i18nKey = CODE_MAP[result?.code];
+
+ if (i18nKey) {
+ return t(i18nKey);
+ }
+
+ if (result?.message) {
+ return result.message;
+ }
+
+ return fallbackKey ? t(fallbackKey) : "";
+}
+// -------------------------------------------------------
+// IMPORT
+// -------------------------------------------------------
+import { getBackendMessage } from "./backendMessages.js";
+
// -----------------------------
// Configuration parameters
// -----------------------------
// Load modals HTML and initialize them
// -----------------------------
export async function loadModals() {
- try {
- const r = await fetch("/modals.html");
- if (!r.ok) throw new Error("Error loading modals");
-
- const html = await r.text();
+ const r = await fetch("/modals.html");
- const container = document.getElementById("modals-container");
- if (!container) {
- console.warn("modals-container not found");
- return;
- }
+ if (!r.ok) {
+ throw new Error("Error loading modals");
+ }
- container.innerHTML = html;
+ const html = await r.text();
- } catch (err) {
- console.error("Modals load error:", err);
+ const container = document.getElementById("modals-container");
+ if (!container) {
+ throw new Error("modals-container not found");
}
+
+ container.innerHTML = html;
}
// -----------------------------
const originalHTML = button.innerHTML;
- // spinner + testo
button.innerHTML = `
<i class="bi bi-arrow-repeat spin"></i>
<span>${workingText}</span>
try {
const result = await serviceFn();
-
- const msg =
- (result && typeof result === "object" && result.message)
- ? result.message
- : defaultSuccessMsg;
-
+ const msg = getBackendMessage(
+ result,
+ defaultSuccessMsg
+ );
showToast(msg, true);
} catch (err) {
- showToast(err?.message || defaultErrorMsg, false);
+ const msg = getBackendMessage(
+ err,
+ defaultErrorMsg
+ );
+ showToast(msg, false);
} finally {
if (!keepDisabled) {
-// Import common js
+// -------------------------------------------------------
+// IMPORT
+// -------------------------------------------------------
import { loadModals, isValidIPv4, isValidIPv6, isValidMAC, showToast, sortTable, initSortableTable, resetSorting, handleSearch, filterTable, clearSearch, handleReload } from './common.js';
-// Import services
-import { serviceReloadDNS, serviceReloadDHCP, serviceGetDHCPLeases, serviceGetDHCPLease, serviceDeleteDHCPLease, serviceGetDevices, serviceGetHost, serviceCreateHost, serviceUpdateHost, serviceDeleteHost } from './services.js';
+import { serviceReloadDNS, serviceReloadDHCP, serviceGetDHCPLease, serviceDeleteDHCPLease, serviceGetDevices, serviceGetHost, serviceCreateHost, serviceUpdateHost, serviceDeleteHost } from './services.js';
+import { loadLanguage, translatePage, t } from "./i18n.js";
+import { getBackendMessage } from "./backendMessages.js";
// -----------------------------
// State variables
viewDevices = [...allDevices];
} catch (err) {
- console.error(err?.message || "Error loading devices");
- showToast(err?.message || "Error loading devices", false);
+ console.error(err?.message || t("devices.list.error"));
+ showToast(err?.message || t("devices.list.error"), false);
allDevices = [];
viewDevices = [];
// hide loader and show table
const trEmpty = document.createElement("tr");
const tdEmpty = document.createElement("td");
tdEmpty.colSpan = 7;
- tdEmpty.textContent = "No devices available.";
+ tdEmpty.textContent = t("devices.empty");
tdEmpty.style.textAlign = "center";
trEmpty.appendChild(tdEmpty);
tbody.appendChild(trEmpty);
} else if (id.startsWith("d-")) {
type = 2;
} else {
- console.error("updateTable: unknown device type:", id);
- showToast("updateTable: unknown device type:", false);
+ console.error(t("devices.unknown.type"), id);
+ showToast(t("devices.unknown.type"), false);
}
const tr = document.createElement("tr");
td.style.verticalAlign = "middle";
const val = (d.dhcp_state ?? "").toString();
- let aria = "";
+ let description = "";
let iconClass = "";
switch (val) {
case "static":
- // Static device
- aria = "Device is static";
+ // Static lease
+ description = t("dhcp.leases.static.description");
iconClass = "bi bi-gear-fill";
break;
case "active":
// DHCP active lease
- aria = "DHCP lease is active";
+ description = t("dhcp.leases.active.description");
iconClass = "bi bi-check-circle-fill";
break;
case "expired":
// DHCP expired lease
- aria = "DHCP lease is expired";
+ description = t("dhcp.leases.expired.description");
iconClass = "bi bi-clock-history";
break;
case "released":
// DHCP released lease
- aria = "DHCP lease is released";
+ description = t("dhcp.leases.released.description");
iconClass = "bi bi-box-arrow-in-right";
break;
case "declined":
// DHCP declined lease
- aria = "DHCP lease is declined";
+ description = t("dhcp.leases.declined.description");
iconClass = "bi bi-x-octagon-fill";
break;
}
if (iconClass) {
const icon = document.createElement("i");
icon.className = iconClass + " icon icon-static";
- icon.setAttribute("aria-hidden", "true");
- icon.setAttribute("title", aria);
+ td.setAttribute("title", description);
+ td.setAttribute("aria-label", description);
td.appendChild(icon);
}
-
+ if (val) td.setAttribute("data-value", val);
tr.appendChild(td);
}
const td = document.createElement("td");
td.style.textAlign = "center";
td.style.verticalAlign = "middle";
+ let description = "";
const active = !!d.active;
td.setAttribute("data-value", active ? "true" : "false");
- td.setAttribute("aria-label", active ? "device active" : "device not active");
const icon = document.createElement("i");
if (active) {
+ description = t("devices.active.description");
icon.className = "bi bi-circle-fill text-success icon icon-static";
- icon.setAttribute("aria-hidden", "true");
- icon.setAttribute("title", "Device is active");
} else {
+ description = t("devices.not.active.description");
icon.className = "bi bi-circle-fill text-danger icon icon-static";
- icon.setAttribute("aria-hidden", "true");
- icon.setAttribute("title", "Device is not active");
}
+ td.setAttribute("title", description);
+ td.setAttribute("aria-label", description);
td.appendChild(icon);
tr.appendChild(td);
}
// Edit Button
const editSpan = document.createElement("span");
+ const editText = t("devices.edit");
editSpan.className = "action-icon";
editSpan.setAttribute("role", "button");
editSpan.tabIndex = 0;
- editSpan.title = "Edit host";
- editSpan.setAttribute("aria-label", "Edit host");
+ editSpan.title = editText;
+ editSpan.setAttribute("aria-label", editText);
editSpan.setAttribute("data-bs-toggle", "modal");
editSpan.setAttribute("data-bs-target", "#addHostModal");
editSpan.setAttribute("data-action", "edit");
// Add Button
const addSpan = document.createElement("span");
+ const addText = t("dhcp.leases.add");
addSpan.className = "action-icon";
addSpan.setAttribute("role", "button");
addSpan.tabIndex = 0;
- addSpan.title = "Add static lease";
- addSpan.setAttribute("aria-label", "Add static lease");
+ addSpan.title = addText;
+ addSpan.setAttribute("aria-label", addText);
addSpan.setAttribute("data-bs-toggle", "modal");
addSpan.setAttribute("data-bs-target", "#addHostModal");
addSpan.setAttribute("data-action", "add");
// Delete Button
const delSpan = document.createElement("span");
+ const deleteText = t("devices.delete");
delSpan.className = "action-icon";
delSpan.setAttribute("role", "button");
delSpan.tabIndex = 0;
- delSpan.title = "Delete device";
- delSpan.setAttribute("aria-label", "Delete device");
+ delSpan.title = deleteText;
+ delSpan.setAttribute("aria-label", deleteText);
delSpan.setAttribute("data-action", "delete");
delSpan.setAttribute("data-device-id", String(id));
{
// dynamic
host = false;
} else {
- throw new Error("Invalid Device ID format for edit");
+ throw new Error(t("devices.edit.invalid_id"));
}
id = Number(id.slice(2));
} else {
- throw new Error("Invalid Device ID for edit");
+ throw new Error(t("devices.edit.invalid_id"));
}
try {
} else {
document.getElementById("hostVisibilityLocal").checked = true;
}
-
} catch (err) {
- console.error(err?.message || "Error loading device");
- showToast(err?.message || "Error loading device", false);
+ console.error(err?.message || t("devices.loaded.error"));
+ showToast(err?.message || t("devices.loaded.error"), false);
}
}
async function saveHost(hostData) {
// Validate hostname
if (!hostData.name.trim()) {
- showToast("Hostname is required", false);
+ showToast(t("validation.name.required"), false);
return false;
}
// Validate IPv4 format
if (!isValidIPv4(hostData.ipv4)) {
- showToast("Invalid IPv4 format", false);
+ showToast(t("validation.ipv4.invalid"), false);
return false;
}
// Validate IPv6 format
if (!isValidIPv6(hostData.ipv6)) {
- showToast("Invalid IPv6 format", false);
+ showToast(t("validation.ipv6.invalid"), false);
return false;
}
// Validate MAC format
if (!isValidMAC(hostData.mac)) {
- showToast("Invalid MAC format", false);
+ showToast(t("validation.mac.invalid"), false);
return false;
}
result = await serviceCreateHost(hostData);
}
- const msg = (typeof result === 'object' && result?.message)
- ? result.message
- : editingHostId !== null
- ? 'Host updated successfully'
- : 'Host created successfully';
+ const msg = getBackendMessage(
+ result,
+ editingHostId !== null
+ ? "devices.updated.ok"
+ : "devices.created.ok"
+ );
showToast(msg, true);
return true;
} catch (err) {
- console.error(err?.message || "Error saving host");
- showToast(err?.message || "Error saving host", false);
+
+ const msg = getBackendMessage(
+ err,
+ editingHostId !== null
+ ? "devices.updated.error"
+ : "devices.created.error"
+ );
+
+ showToast(msg, false);
}
return false;
}
} catch (err) {
- console.error(err?.message || "Error saving host");
- showToast(err?.message || "Error saving host", false);
+ console.error(
+ err?.message ||
+ (editingHostId !== null
+ ? t("devices.updated.error")
+ : t("devices.created.error"))
+ );
+
+ showToast(
+ err?.message ||
+ (editingHostId !== null
+ ? t("devices.updated.error")
+ : t("devices.created.error")),
+ false
+ );
}
return false;
let id = el.dataset.deviceId;
if (!id) {
- console.warn('Delete: device id not valid for delete:', id);
- showToast('Device id not valid for delete', false);
+ showToast(t("devices.delete.invalid_id"), false);
return;
}
// dynamic
host = false;
} else {
- throw new Error("Invalid Device ID format for edit");
+ throw new Error(t("devices.edit.invalid_id"));
}
id = Number(id.slice(2));
} else {
- throw new Error("Invalid Device ID for edit");
+ throw new Error(t("devices.edit.invalid_id"));
}
try {
- if(host){
- const result = await serviceDeleteHost(id);
+ let result;
+ if(host){
+ result = await serviceDeleteHost(id);
} else {
- const result = await serviceDeleteDHCPLease(id);
+ result = await serviceDeleteDHCPLease(id);
}
- const msg = (typeof result === 'object' && result?.message)
- ? result.message
- : 'Host deleted successfully';
-
+ const msg = getBackendMessage(result, "devices.deleted.ok");
showToast(msg, true);
// Reload devices
return true;
} catch (err) {
- console.error(err?.message || "Error deleting device");
- showToast(err?.message || "Error deleting device", false);
+ console.error(err?.message || t("devices.deleted.error"));
+ showToast(err?.message || t("devices.deleted.error"), false);
}
return false;
delete: (e, el) => {
handleDeleteDevice(e, el);
},
- // Edit host
+ // Add device
+ add: () => {
+ // handled by bootstrap modal show event
+ },
+ // Edit device
edit: () => {
// handled by bootstrap modal show event
},
await handleReload(
el,
serviceReloadDNS,
- "DNS reload successfully",
- "Error reloading DNS",
- "Reloading DNS..."
+ t("dns.reload.ok"),
+ t("dns.reload.error"),
+ t("dns.reload.progress")
);
},
// Reload DHCP
await handleReload(
el,
serviceReloadDHCP,
- "DHCP reload successfully",
- "Error reloading DHCP",
- "Reloading DHCP..."
+ t("dhcp.reload.ok"),
+ t("dhcp.reload.error"),
+ t("dhcp.reload.progress")
);
},
};
// -----------------------------
async function initApp() {
+ // Loading translation
+ try {
+ await loadLanguage();
+ } catch (err) {
+ console.error(err?.message || t("app.translation.error"));
+ showToast(t("app.translation.error"), false);
+ }
+
// Load modals (Bootstrap 5 requires JS initialization for dynamic content)
try {
await loadModals();
} catch (err) {
- console.error(err?.message || "Error loading modals");
- showToast(err?.message || "Error loading modals", false);
+ console.error(err?.message || t("app.modals.error"));
+ showToast(t("app.modals.error"), false);
}
+ // Translate page
+ translatePage();
+
// Load data (devices)
try {
await fetchDevices();
updateTable();
} catch (err) {
- console.error(err?.message || "Error loading devices");
- showToast(err?.message || "Error loading devices", false);
+ console.error(err?.message || t("devices.list.error"));
+ showToast(err?.message || t("devices.list.error"), false);
}
initUI();
try {
await editHost(id);
} catch (err) {
- showToast(err?.message || "Error loading host", false);
+ showToast(err?.message || t("devices.loaded.error"), false);
// Close modal
modalEl.addEventListener('shown.bs.modal', () => {
closeAddHostModal();
try {
await handler(e, el);
} catch (err) {
- console.error(err?.message || 'Action error');
- showToast(err?.message || 'Action error', false);
+ console.error(err?.message || t("app.action.error"));
+ showToast(err?.message || t("app.action.error"), false);
}
}
-// Import common js
+// -------------------------------------------------------
+// IMPORT
+// -------------------------------------------------------
import { loadModals, isValidIPv4, isValidIPv6, isValidMAC, showToast, sortTable, initSortableTable, resetSorting, handleSearch, filterTable, clearSearch, handleReload } from './common.js';
-// Import services
import { serviceReloadDNS, serviceReloadDHCP, serviceGetHosts, serviceGetHost, serviceCreateHost, serviceUpdateHost, serviceDeleteHost } from './services.js';
+import { loadLanguage, translatePage, t } from "./i18n.js";
+import { getBackendMessage } from "./backendMessages.js";
// -----------------------------
// State variables
viewHosts = [...allHosts];
} catch (err) {
- console.error(err?.message || "Error loading hosts");
- showToast(err?.message || "Error loading hosts", false);
+ console.error(err?.message || t("hosts.list.error"));
+ showToast(err?.message || t("hosts.list.error"), false);
allHosts = [];
viewHosts = [];
// hide loader and show table
const trEmpty = document.createElement("tr");
const tdEmpty = document.createElement("td");
tdEmpty.colSpan = 6;
- tdEmpty.textContent = "No hosts available.";
+ tdEmpty.textContent = t("hosts.empty");
tdEmpty.style.textAlign = "center";
trEmpty.appendChild(tdEmpty);
tbody.appendChild(trEmpty);
// SSL icon
//
const sslEnabled = !!h.ssl_enabled;
- td.setAttribute("data-value", sslEnabled ? "true" : "false");
- td.setAttribute("aria-label", sslEnabled ? "SSL attivo" : "SSL non attivo");
- const icon = document.createElement("i");
+ const wrapperSslIcon = document.createElement("span");
+ const sslIcon = document.createElement("i");
+ let description = "";
if (sslEnabled) {
- icon.className = "bi bi-shield-lock-fill icon icon-static";
- icon.setAttribute("aria-hidden", "true");
- icon.setAttribute("title", "SSL certificate enabled");
+ sslIcon.className = "bi bi-shield-lock-fill icon icon-static";
+ description = t("hosts.ssl.enabled");
+ wrapperSslIcon.setAttribute("title", description);
+ wrapperSslIcon.setAttribute("aria-label", description);
} else {
- icon.className = "bi bi-shield-lock-fill icon icon-static icon-placeholder";
- icon.setAttribute("aria-hidden", "true");
+ sslIcon.className = "bi bi-shield-lock-fill icon icon-static icon-placeholder";
+ sslIcon.setAttribute("aria-hidden", "true");
}
- td.appendChild(icon);
+ wrapperSslIcon.appendChild(sslIcon);
+ td.appendChild(wrapperSslIcon);
//
// visibility icon
//
const ext = (h.visibility ?? "").toString();
- let aria = "";
+ const wrapperVisibilityIcon = document.createElement("span");
+ const visibilityIcon = document.createElement("i");
let iconClass = "";
switch (ext) {
case "0":
// Only local (A record internally resolved)
- aria = "Only local (A record internally resolved)";
+ description = t("hosts.visibility.local.description");
iconClass = "bi bi-hdd-network";
break;
case "1":
// Local and external (A record internally resolved, A externally)
- aria = "Internal and external are identical";
+ description = t("hosts.visibility.global.description");
iconClass = "bi bi-globe2";
break;
case "2":
// CNAME -> DDNS / external_name
- aria = "External is a CNAME to external_name";
+ description = t("hosts.visibility.alias.description");
iconClass = "bi bi-link-45deg";
break;
}
if (iconClass) {
- const icon = document.createElement("i");
- icon.className = iconClass + " icon icon-static";
- icon.setAttribute("aria-hidden", "true");
- icon.setAttribute("title", aria);
- td.appendChild(icon);
+ visibilityIcon.className = iconClass + " icon icon-static";
+ wrapperVisibilityIcon.setAttribute("title", description);
+ wrapperVisibilityIcon.setAttribute("aria-label", description);
+ wrapperVisibilityIcon.appendChild(visibilityIcon);
+ td.appendChild(wrapperVisibilityIcon);
}
-
+ if (ext) td.setAttribute("data-value", ext);
tr.appendChild(td);
}
// Edit Button
const editSpan = document.createElement("span");
+ const editText = t("hosts.edit");
editSpan.className = "action-icon";
editSpan.setAttribute("role", "button");
editSpan.tabIndex = 0;
- editSpan.title = "Edit host";
- editSpan.setAttribute("aria-label", "Edit host");
+ editSpan.title = editText;
+ editSpan.setAttribute("aria-label", editText);
editSpan.setAttribute("data-bs-toggle", "modal");
editSpan.setAttribute("data-bs-target", "#addHostModal");
editSpan.setAttribute("data-action", "edit");
// Delete Button
const delSpan = document.createElement("span");
+ const deleteText = t("hosts.delete");
delSpan.className = "action-icon";
delSpan.setAttribute("role", "button");
delSpan.tabIndex = 0;
- delSpan.title = "Delete host";
- delSpan.setAttribute("aria-label", "Delete host");
+ delSpan.title = deleteText;
+ delSpan.setAttribute("aria-label", deleteText);
delSpan.setAttribute("data-action", "delete");
delSpan.setAttribute("data-host-id", String(id));
{
} else {
document.getElementById("hostVisibilityLocal").checked = true;
}
-
} catch (err) {
- console.error(err?.message || "Error loading host");
- showToast(err?.message || "Error loading host", false);
+ console.error(err?.message || t("hosts.loaded.error"));
+ showToast(err?.message || t("hosts.loaded.error"), false);
}
}
async function saveHost(hostData) {
// Validate hostname
if (!hostData.name.trim()) {
- showToast("Hostname is required", false);
+ showToast(t("validation.name.required"), false);
return false;
}
// Validate IPv4 format
if (!isValidIPv4(hostData.ipv4)) {
- showToast("Invalid IPv4 format", false);
+ showToast(t("validation.ipv4.invalid"), false);
return false;
}
// Validate IPv6 format
if (!isValidIPv6(hostData.ipv6)) {
- showToast("Invalid IPv6 format", false);
+ showToast(t("validation.ipv6.invalid"), false);
return false;
}
// Validate MAC format
if (!isValidMAC(hostData.mac)) {
- showToast("Invalid MAC format", false);
+ showToast(t("validation.mac.invalid"), false);
return false;
}
result = await serviceCreateHost(hostData);
}
- const msg = (typeof result === 'object' && result?.message)
- ? result.message
- : editingHostId !== null
- ? 'Host updated successfully'
- : 'Host created successfully';
+ const msg = getBackendMessage(
+ result,
+ editingHostId !== null
+ ? "hosts.updated.ok"
+ : "hosts.created.ok"
+ );
showToast(msg, true);
return true;
} catch (err) {
- console.error(err?.message || "Error saving host");
- showToast(err?.message || "Error saving host", false);
+
+ const msg = getBackendMessage(
+ err,
+ editingHostId !== null
+ ? "hosts.updated.error"
+ : "hosts.created.error"
+ );
+
+ showToast(msg, false);
}
return false;
}
} catch (err) {
- console.error(err?.message || "Error saving host");
- showToast(err?.message || "Error saving host", false);
+ console.error(
+ err?.message ||
+ (editingHostId !== null
+ ? t("hosts.updated.error")
+ : t("hosts.created.error"))
+ );
+
+ showToast(
+ err?.message ||
+ (editingHostId !== null
+ ? t("hosts.updated.error")
+ : t("hosts.created.error")),
+ false
+ );
}
return false;
// Get host ID
const id = Number(el.dataset.hostId);
if (!Number.isFinite(id)) {
- showToast('Host id not valid for delete', false);
+ showToast(t("hosts.delete.invalid_id"), false);
return;
}
try {
const result = await serviceDeleteHost(id);
-
- const msg = (typeof result === 'object' && result?.message)
- ? result.message
- : 'Host deleted successfully';
-
+ const msg = getBackendMessage(result, "hosts.deleted.ok");
showToast(msg, true);
// Reload hosts
return true;
} catch (err) {
- console.error(err?.message || "Error deleting host");
- showToast(err?.message || "Error deleting host", false);
+ console.error(err?.message || t("hosts.deleted.error"));
+ showToast(err?.message || t("hosts.deleted.error"), false);
}
return false;
await handleReload(
el,
serviceReloadDNS,
- "DNS reload successfully",
- "Error reloading DNS",
- "Reloading DNS..."
+ t("dns.reload.ok"),
+ t("dns.reload.error"),
+ t("dns.reload.progress")
);
},
// Reload DHCP
await handleReload(
el,
serviceReloadDHCP,
- "DHCP reload successfully",
- "Error reloading DHCP",
- "Reloading DHCP..."
+ t("dhcp.reload.ok"),
+ t("dhcp.reload.error"),
+ t("dhcp.reload.progress")
);
},
};
// -----------------------------
async function initApp() {
+ // Loading translation
+ try {
+ await loadLanguage();
+ } catch (err) {
+ console.error(err?.message || t("app.translation.error"));
+ showToast(t("app.translation.error"), false);
+ }
+
// Load modals (Bootstrap 5 requires JS initialization for dynamic content)
try {
await loadModals();
} catch (err) {
- console.error(err?.message || "Error loading modals");
- showToast(err?.message || "Error loading modals", false);
+ console.error(err?.message || t("app.modals.error"));
+ showToast(t("app.modals.error"), false);
}
+ // Translate page
+ translatePage();
+
// Load data (hosts)
try {
await fetchHosts();
updateTable();
} catch (err) {
- console.error(err?.message || "Error loading hosts");
- showToast(err?.message || "Error loading hosts", false);
+ console.error(err?.message || t("hosts.list.error"));
+ showToast(err?.message || t("hosts.list.error"), false);
}
initUI();
try {
await editHost(id);
} catch (err) {
- showToast(err?.message || "Error loading host", false);
+ showToast(err?.message || t("hosts.loaded.error"), false);
// Close modal
modalEl.addEventListener('shown.bs.modal', () => {
closeAddHostModal();
try {
await handler(e, el);
} catch (err) {
- console.error(err?.message || 'Action error');
- showToast(err?.message || 'Action error', false);
+ console.error(err?.message || t("app.action.error"));
+ showToast(err?.message || t("app.action.error"), false);
}
}
--- /dev/null
+// -------------------------------------------------------
+// IMPORT
+// -------------------------------------------------------
+import { apiGet } from "./api.js";
+
+let _language = "en";
+let _translations = {};
+
+// -------------------------------------------------------
+// Load translations from backend
+// -------------------------------------------------------
+export async function loadLanguage() {
+ const data = await apiGet(
+ "/api/i18n",
+ "Unable to load translations"
+ );
+
+ _language = data.language;
+ _translations = data.translations;
+}
+
+// -------------------------------------------------------
+// Get current language
+// -------------------------------------------------------
+export function getLanguage() {
+ return _language;
+}
+
+// -------------------------------------------------------
+// Translate key
+// -------------------------------------------------------
+export function t(key) {
+ return _translations[key] || key;
+}
+
+// -------------------------------------------------------
+// Translate DOM elements
+// -------------------------------------------------------
+export function translatePage() {
+
+ // Page Title
+ const pageTitleKey = document.body.dataset.pageTitle;
+ if (pageTitleKey) {
+ document.title = `${t(pageTitleKey)} - ${t("app.name")}`;
+ }
+
+ // Generic Content
+ document.querySelectorAll("[data-i18n]").forEach(element => {
+ const key = element.dataset.i18n;
+ element.textContent = t(key);
+ });
+
+ // title
+ document.querySelectorAll("[data-i18n-title]").forEach(element => {
+ element.title = t(element.dataset.i18nTitle);
+ });
+
+ // aria-label
+ document.querySelectorAll("[data-i18n-aria-label]").forEach(element => {
+ element.setAttribute(
+ "aria-label",
+ t(element.dataset.i18nAriaLabel)
+ );
+ });
+
+ // placeholder
+ document.querySelectorAll("[data-i18n-placeholder]").forEach(element => {
+ element.placeholder = t(
+ element.dataset.i18nPlaceholder
+ );
+ });
+}
// -------------------------------------------------------
import { loadModals, showToast, showConfirmModal, handleReload } from './common.js';
import { serviceIsAlive, serviceCheckHealth , serviceReloadDNS, serviceReloadDHCP, serviceBackupCreate, serviceBackupList, serviceBackupRestore, serviceDeleteBackup, serviceDownloadBackup, serviceUploadBackup } from './services.js';
+import { loadLanguage, translatePage, t } from "./i18n.js";
+import { getBackendMessage } from "./backendMessages.js";
+
+// -------------------------------------------------------
+// Localize health status for display in the UI
+// -------------------------------------------------------
+function localizeHealthStatus(status) {
+ const norm = String(status || "").toLowerCase();
+
+ switch (norm) {
+ case "healthy":
+ return t("health.status.healthy");
+
+ case "degraded":
+ return t("health.status.degraded");
+
+ case "unhealthy":
+ return t("health.status.unhealthy");
+
+ default:
+ return t("health.status.unknown");
+ }
+}
// -------------------------------------------------------
// BACKUP MODAL OPEN/CLOSE
modal.style.display = 'flex';
const tbody = document.getElementById("backupList");
- if (tbody) {
- tbody.innerHTML = `
- <tr>
- <td colspan="5" class="text-center text-muted">
- Loading backups...
- </td>
- </tr>
- `;
- }
+ if (!tbody) return;
+ tbody.innerHTML = `
+ <tr>
+ <td colspan="5" class="text-center text-muted">
+ ${t("backup.loading")}
+ </td>
+ </tr>
+ `;
// Refresh backup list
try {
const result = await serviceBackupList();
renderBackupList(result);
} catch (err) {
- showToast(err?.message || "Error refreshing backup list", false);
+ const msg = getBackendMessage(
+ err,
+ "backup.refresh_error"
+ );
+ showToast(msg, false);
}
}
export async function serviceCheckAbout() {
const pills = document.querySelectorAll('.btn-api');
- if (!pills.length) return;
+ if (!pills.length) return false;
const ok = await serviceIsAlive();
pills.forEach(pill => {
if (ok) {
- pill.textContent = 'API OK';
+ pill.textContent = t("header.api_status_online");
pill.classList.remove('btn-outline-primary');
pill.classList.add('btn-primary');
} else {
- pill.textContent = 'API OFFLINE';
+ pill.textContent = t("header.api_status_offline");
}
});
// -------------------------------------------------------
function renderBackupList(data) {
const tbody = document.getElementById("backupList");
+ if (!tbody) return;
tbody.innerHTML = "";
if (!data?.backups || !Array.isArray(data.backups) || data.backups.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="5" class="text-center text-muted">
- No backups available
+ ${t("backup.empty")}
</td>
</tr>
`;
tdActions.classList.add("text-end");
// download button
const downloadBtn = document.createElement("button");
+ const downloadText = t("backup.download.file");
downloadBtn.className = "btn btn-sm btn-outline-primary me-2";
- downloadBtn.title = "Download backup";
+ downloadBtn.title = downloadText;
downloadBtn.innerHTML = `<i class="bi bi-download"></i>`;
+ downloadBtn.setAttribute("aria-label", downloadText);
downloadBtn.setAttribute("data-action", "downloadBackup");
downloadBtn.setAttribute("data-id", b.name);
tdActions.appendChild(downloadBtn);
// delete button
const deleteBtn = document.createElement("button");
+ const deleteText = t("backup.delete.file");
deleteBtn.className = "btn btn-sm btn-outline-danger";
- deleteBtn.title = "Delete backup";
+ deleteBtn.title = deleteText;
deleteBtn.innerHTML = `<i class="bi bi-trash-fill"></i>`;
+ deleteBtn.setAttribute("aria-label", deleteText);
deleteBtn.setAttribute("data-action", "deleteBackup");
deleteBtn.setAttribute("data-id", b.name);
tdActions.appendChild(deleteBtn);
Promise.resolve()
.then(() => serviceCheckHealth())
.then((data) => {
- // Se serviceCheckHealth ritorna true o {message}, non abbiamo i dettagli: mostra un messaggio
+ // Verify that the response contains health details
const isDetailed =
data && typeof data === 'object' &&
('status' in data || 'latency_ms' in data || 'database' in data);
if (!isDetailed) {
- throw new Error('Health details not available');
+ throw new Error(t("health.details_unavailable"));
}
renderHealth(data);
.catch((err) => {
loadingEl?.classList?.add('d-none');
errorEl?.classList?.remove('d-none');
- showToast(err?.message || 'Error while fetching health status', false);
+ showToast(err?.message || t("health.error"), false);
console.error(err);
});
}
if (!badgeEl) return;
const norm = String(status || '').toLowerCase();
+
let cls = 'bg-secondary';
- if (norm === 'ok' || norm === 'healthy' || norm === 'up') cls = 'bg-success';
- if (norm === 'warn' || norm === 'warning' || norm === 'degraded') cls = 'bg-warning text-dark';
- if (norm === 'down' || norm === 'error' || norm === 'fail' || norm === 'critical') cls = 'bg-danger';
+
+ if (norm === 'healthy') {
+ cls = 'bg-success';
+ } else if (norm === 'degraded') {
+ cls = 'bg-warning text-dark';
+ } else if (norm === 'unhealthy') {
+ cls = 'bg-danger';
+ }
badgeEl.className = `badge rounded-pill ${cls}`;
- badgeEl.textContent = norm || 'unknown';
+ badgeEl.textContent = localizeHealthStatus(norm);
}
function renderHealth(data) {
setHealthBadge(status);
if (updatedAtEl) {
const now = new Date();
- updatedAtEl.textContent = `Updated at ${now.toLocaleTimeString()}`;
+ updatedAtEl.textContent = `${t("health.updated_at")} ${now.toLocaleTimeString()}`;
}
const rows = [
- { label: 'Status', value: status },
- { label: 'Latency', value: (typeof latency === 'number') ? `${latency} ms` : '—' },
- { label: 'DB Status', value: dbStatus },
- { label: 'DB Version', value: dbVersion },
- { label: 'DB Tables', value: dbTables },
- { label: 'DB Size', value: dbSize },
+ { label: t("health.status"), value: localizeHealthStatus(status) },
+ { label: t("health.latency"), value: (typeof latency === "number") ? `${latency} ms` : "—" },
+ { label: t("health.db_status"), value: localizeHealthStatus(dbStatus) },
+ { label: t("health.db_version"), value: dbVersion },
+ { label: t("health.db_tables"), value: dbTables },
+ { label: t("health.db_size"), value: dbSize },
];
if (summaryEl) {
// Create Backup
startBackup: async (e, el) => {
const btn = el;
- const modal = document.getElementById('backupModal');
if (!btn) return;
const originalLabel = label?.textContent ?? '';
btn.disabled = true;
- label.textContent = ' Exporting…';
+ label.textContent = t("backup.create.progress");
try {
const result = await serviceBackupCreate();
- const msg = (typeof result === 'object' && result?.message)
- ? result.message
- : 'Backup completed successfully';
- showToast(msg, !result?.partial);
+ const success = result?.status !== 'partial';
+ const msg = getBackendMessage(
+ result,
+ success
+ ? "backup.create_ok"
+ : "backup.create_partial"
+ );
+ showToast(msg, success);
} catch (err) {
- showToast(err?.message || "Error performing backup", false);
+ const msg = getBackendMessage(
+ err,
+ "backup.create_error"
+ );
+ showToast(msg, false);
} finally {
label.textContent = originalLabel;
btn.disabled = false;
const result = await serviceBackupList();
renderBackupList(result);
} catch (err) {
- showToast(err?.message || "Error refreshing backup list", false);
+ const msg = getBackendMessage(
+ err,
+ "backup.refresh_error"
+ );
+ showToast(msg, false);
}
},
// Restore Backup
startRestore: async (e, el) => {
const btn = el;
- const modal = document.getElementById('backupModal');
+ //const modal = document.getElementById('backupModal');
const id = getSelectedBackup();
if (!id) {
- showToast('Select a backup', false);
+ showToast(t("backup.select"), false);
return;
}
const originalLabel = label?.textContent ?? '';
btn.disabled = true;
- label.textContent = ' Restoring…';
+ label.textContent = t("backup.restore.progress");
try {
const result = await serviceBackupRestore(id);
- const msg = (typeof result === 'object' && result?.message)
- ? result.message
- : 'Restore completed successfully';
- showToast(msg, !result?.partial);
+ const success = result?.status !== 'partial';
+ const msg = getBackendMessage(
+ result,
+ success
+ ? "backup.restore_ok"
+ : "backup.restore_partial"
+ );
+ showToast(msg, success);
// Close modal
//if (modal) modal.style.display = 'none';
} catch (err) {
- showToast(err?.message || "Error performing restore", false);
+ const msg = getBackendMessage(
+ err,
+ "backup.restore_error"
+ );
+ showToast(msg, false);
} finally {
label.textContent = originalLabel;
btn.disabled = false;
const id = el.dataset.id;
if (!id) return;
- const confirmed = await showConfirmModal(`Delete backup "${id}" ?`);
+ const confirmed = await showConfirmModal(t("backup.delete_confirm").replace("{id}", id));
if (!confirmed) return;
try {
const result = await serviceDeleteBackup(id);
-
- const msg = (typeof result === 'object' && result?.message)
- ? result.message
- : 'Backup deleted successfully';
-
+ const msg = getBackendMessage(
+ result,
+ "backup.delete_ok"
+ );
showToast(msg, true);
} catch (err) {
- console.error(err);
- showToast(err?.message || "Error deleting backup", false);
+ const msg = getBackendMessage(
+ err,
+ "backup.delete_error"
+ );
+ showToast(msg, false);
}
// Refresh backup list
try {
const result = await serviceBackupList();
renderBackupList(result);
} catch (err) {
- showToast(err?.message || "Error refreshing backup list", false);
+ const msg = getBackendMessage(
+ err,
+ "backup.refresh_error"
+ );
+ showToast(msg, false);
}
},
refreshBackupList: async () => {
try {
const result = await serviceBackupList();
- const msg = (typeof result === 'object' && result?.message)
- ? result.message
- : 'Backup list refreshed successfully';
+ const msg = getBackendMessage(
+ result,
+ "backup.refresh_ok"
+ );
showToast(msg, true);
renderBackupList(result);
} catch (err) {
- showToast(err?.message || "Error refreshing backup list", false);
+ const msg = getBackendMessage(
+ err,
+ "backup.refresh_error"
+ );
+ showToast(msg, false);
}
},
// Download Backup
const msg = (typeof result === 'object' && result?.message)
? result.message
- : 'Backup downloaded successfully';
+ : t("backup.download_ok");
showToast(msg, true);
} catch (err) {
console.error(err);
- showToast(err?.message || "Error downloading backup", false);
+ showToast(err?.message || t("backup.download_error"), false);
}
},
// Upload Backup
uploadBackup: async (e, el) => {
const input = document.getElementById('backupUploadInput');
if (!input?.files?.length) {
- showToast("Select a file first", false);
+ showToast(t("backup.select_file"), false);
return;
}
const msg = (result?.message)
? result.message
- : 'Backup uploaded successfully';
+ : t("backup.update_ok");
showToast(msg, true);
-
- console.log("Uploaded backup ID:", result?.backup_id);
-
input.value = '';
} catch (err) {
- showToast(err?.message || "Error uploading backup", false);
+ showToast(err?.message || t("backup.update_error"), false);
} finally {
if (icon && originalClass) {
icon.className = originalClass;
const result = await serviceBackupList();
renderBackupList(result);
} catch (err) {
- showToast(err?.message || "Error refreshing backup list", false);
+ const msg = getBackendMessage(
+ err,
+ "backup.refresh_error"
+ );
+ showToast(msg, false);
}
},
openBackupModal, // managed by boostrap
await handleReload(
el,
serviceReloadDNS,
- "DNS reload successfully",
- "Error reloading DNS",
- "Reloading DNS..."
+ t("dns.reload.ok"),
+ t("dns.reload.error"),
+ t("dns.reload.progress")
);
},
// Reload DHCP
await handleReload(
el,
serviceReloadDHCP,
- "DHCP reload successfully",
- "Error reloading DHCP",
- "Reloading DHCP..."
+ t("dhcp.reload.ok"),
+ t("dhcp.reload.error"),
+ t("dhcp.reload.progress")
);
},
// Check API status
apiCheck: async () => {
const result = await serviceCheckAbout();
if(result) {
- showToast('API status updated succesfully', true);
+ showToast(t("health.update.ok"), true);
} else {
- showToast('Error updating API status', false);
+ showToast(t("health.update.error"), false);
}
},
// Health
try {
await loadModals();
} catch (err) {
- console.error(err?.message || "Error loading modals");
- showToast(err?.message || "Error loading modals", false);
+ console.error(err?.message || t("app.modals.error"));
+ showToast(t("app.modals.error"), false);
}
+ // translate modals
+ translatePage();
+
// Init Backup Modal (backdrop click to close)
initBackupModal();
});
const handler = actionHandlers[action];
if (!handler) return;
+ // Execute handler
try {
await handler(e, el);
} catch (err) {
- console.error(err?.message || 'Action error');
- showToast(err?.message || 'Action error', false);
+ console.error(err?.message || t("app.action.error"));
+ showToast(err?.message || t("app.action.error"), false);
}
});
setTimeout(periodicTest, 10000);
}
+// Loading translation
+try {
+ await loadLanguage();
+} catch (err) {
+ console.error(err?.message || t("app.translation.error"));
+ showToast(t("app.translation.error"), false);
+}
+
+// Translate page
+translatePage();
+
+// Periodic Test
periodicTest();
-// Import common js
+// -------------------------------------------------------
+// IMPORT
+// -------------------------------------------------------
import { loadModals, isValidIPv4, isValidIPv6, isValidMAC, showToast, sortTable, initSortableTable, resetSorting, handleSearch, filterTable, clearSearch, handleReload } from './common.js';
-// Import services
import { serviceReloadDNS, serviceReloadDHCP, serviceGetDHCPLeases, serviceDeleteDHCPLease, serviceGetDHCPLease, serviceCreateHost} from './services.js';
+import { loadLanguage, translatePage, t } from "./i18n.js";
+import { getBackendMessage } from "./backendMessages.js";
// -----------------------------
// State variables
viewLeases = [...allLeases];
} catch (err) {
- console.error(err?.message || "Error loading leases");
- showToast(err?.message || "Error loading leases", false);
+ console.error(err?.message || t("dhcp.leases.list.error"));
+ showToast(err?.message || t("dhcp.leases.list.error"), false);
allLeases = [];
viewLeases = [];
// hide loader and show table
}
// -----------------------------
-// Update table with current hosts
+// Update table with current leases
// -----------------------------
function updateTable () {
const loader = document.getElementById("loader");
const trEmpty = document.createElement("tr");
const tdEmpty = document.createElement("td");
tdEmpty.colSpan = 7;
- tdEmpty.textContent = "No leases available.";
+ tdEmpty.textContent = t("dhcp.leases.empty");
tdEmpty.style.textAlign = "center";
trEmpty.appendChild(tdEmpty);
tbody.appendChild(trEmpty);
td.style.verticalAlign = "middle";
const val = (l.dhcp_state ?? "").toString();
- let aria = "";
+ let description = "";
let iconClass = "";
switch (val) {
case "active":
// DHCP active lease
- aria = "DHCP lease is active";
+ description = t("dhcp.leases.active.description");
iconClass = "bi bi-check-circle-fill";
break;
case "expired":
// DHCP expired lease
- aria = "DHCP lease is expired";
+ description = t("dhcp.leases.expired.description");
iconClass = "bi bi-clock-history";
break;
case "released":
// DHCP released lease
- aria = "DHCP lease is released";
+ description = t("dhcp.leases.released.description");
iconClass = "bi bi-box-arrow-in-right";
break;
case "declined":
// DHCP declined lease
- aria = "DHCP lease is declined";
+ description = t("dhcp.leases.declined.description");
iconClass = "bi bi-x-octagon-fill";
break;
}
if (iconClass) {
const icon = document.createElement("i");
icon.className = iconClass + " icon icon-static";
- icon.setAttribute("aria-hidden", "true");
- icon.setAttribute("title", aria);
+ td.setAttribute("title", description);
+ td.setAttribute("aria-label", description);
td.appendChild(icon);
}
-
+ if (val) td.setAttribute("data-value", val);
tr.appendChild(td);
}
// Add Button
const addSpan = document.createElement("span");
+ const addText = t("dhcp.leases.add");
addSpan.className = "action-icon";
addSpan.setAttribute("role", "button");
addSpan.tabIndex = 0;
- addSpan.title = "Add static lease";
- addSpan.setAttribute("aria-label", "Add static lease");
+ addSpan.title = addText;
+ addSpan.setAttribute("aria-label", addText);
addSpan.setAttribute("data-bs-toggle", "modal");
addSpan.setAttribute("data-bs-target", "#addHostModal");
addSpan.setAttribute("data-action", "add");
// Delete Button
const delSpan = document.createElement("span");
+ const deleteText = t("dhcp.leases.delete");
delSpan.className = "action-icon";
delSpan.setAttribute("role", "button");
delSpan.tabIndex = 0;
- delSpan.title = "Delete lease";
- delSpan.setAttribute("aria-label", "Delete lease");
+ delSpan.title = deleteText;
+ delSpan.setAttribute("aria-label", deleteText);
delSpan.setAttribute("data-action", "delete");
delSpan.setAttribute("data-lease-id", String(id));
{
}
} catch (err) {
- console.error(err?.message || "Error loading lease");
- showToast(err?.message || "Error loading lease", false);
+ console.error(err?.message || t("dhcp.leases.loaded.error"));
+ showToast(err?.message || t("dhcp.leases.loaded.error"), false);
}
}
async function saveHost(hostData) {
// Validate hostname
if (!hostData.name.trim()) {
- showToast("Hostname is required", false);
+ showToast(t("validation.name.required"), false);
return false;
}
// Validate IPv4 format
if (!isValidIPv4(hostData.ipv4)) {
- showToast("Invalid IPv4 format", false);
+ showToast(t("validation.ipv4.invalid"), false);
return false;
}
// Validate IPv6 format
if (!isValidIPv6(hostData.ipv6)) {
- showToast("Invalid IPv6 format", false);
+ showToast(t("validation.ipv6.invalid"), false);
return false;
}
// Validate MAC format
if (!isValidMAC(hostData.mac)) {
- showToast("Invalid MAC format", false);
+ showToast(t("validation.mac.invalid"), false);
return false;
}
try {
const result = await serviceCreateHost(hostData);
- const msg = (typeof result === 'object' && result?.message)
- ? result.message
- : 'Host created successfully';
+ const msg = getBackendMessage(
+ result,
+ "hosts.created.ok"
+ );
showToast(msg, true);
return true;
} catch (err) {
- console.error(err?.message || "Error saving host");
- showToast(err?.message || "Error saving host", false);
+
+ const msg = getBackendMessage(
+ err,
+ "hosts.created.error"
+ );
+
+ showToast(msg, false);
}
return false;
const ok = await saveHost(data);
if (ok !== false) {
- // close modal and reload hosts
+ // close modal and reload leases
closeAddHostModal();
await fetchLeases();
updateTable();
}
} catch (err) {
- console.error(err?.message || "Error saving host");
- showToast(err?.message || "Error saving host", false);
+ console.error(err?.message || t("hosts.created.error"));
+ showToast(err?.message || t("hosts.created.error"), false);
}
return false;
// Get lease ID
const id = Number(el.dataset.leaseId);
if (!Number.isFinite(id)) {
- showToast('Lease id not valid for delete', false);
+ showToast(t("dhcp.leases.delete.invalid_id"), false);
return;
}
try {
const result = await serviceDeleteDHCPLease(id);
- const msg = (typeof result === 'object' && result?.message)
- ? result.message
- : 'Lease deleted successfully';
-
+ const msg = getBackendMessage(result, "dhcp.leases.deleted.ok");
showToast(msg, true);
// Reload leases
return true;
} catch (err) {
- console.error(err?.message || "Error deleting lease");
- showToast(err?.message || "Error deleting lease", false);
+ console.error(err?.message || t("dhcp.leases.deleted.error"));
+ showToast(err?.message || t("dhcp.leases.deleted.error"), false);
}
return false;
await handleReload(
el,
serviceReloadDNS,
- "DNS reload successfully",
- "Error reloading DNS",
- "Reloading DNS..."
+ t("dns.reload.ok"),
+ t("dns.reload.error"),
+ t("dns.reload.progress")
);
},
// Reload DHCP
await handleReload(
el,
serviceReloadDHCP,
- "DHCP reload successfully",
- "Error reloading DHCP",
- "Reloading DHCP..."
+ t("dhcp.reload.ok"),
+ t("dhcp.reload.error"),
+ t("dhcp.reload.progress")
);
},
};
// -----------------------------
async function initApp() {
+ // Loading translation
+ try {
+ await loadLanguage();
+ } catch (err) {
+ console.error(err?.message || t("app.translation.error"));
+ showToast(t("app.translation.error"), false);
+ }
+
// Load modals (Bootstrap 5 requires JS initialization for dynamic content)
try {
await loadModals();
} catch (err) {
- console.error(err?.message || "Error loading modals");
- showToast(err?.message || "Error loading modals", false);
+ console.error(err?.message || t("app.modals.error"));
+ showToast(t("app.modals.error"), false);
}
+ // Translate page
+ translatePage();
+
// Load data (leases)
try {
await fetchLeases();
updateTable();
} catch (err) {
- console.error(err?.message || "Error loading dhcp leases");
- showToast(err?.message || "Error loading dhcp leases", false);
+ console.error(err?.message || t("dhcp.leases.list.error"));
+ showToast(err?.message || t("dhcp.leases.list.error"), false);
}
initUI();
try {
await addHost(id);
} catch (err) {
- showToast(err?.message || "Error loading host", false);
+ showToast(err?.message || t("dhcp.leases.loaded.error"), false);
// Close modal
modalEl.addEventListener('shown.bs.modal', () => {
closeAddHostModal();
try {
await handler(e, el);
} catch (err) {
- console.error(err?.message || 'Action error');
- showToast(err?.message || 'Action error', false);
+ console.error(err?.message || t("app.action.error"));
+ showToast(err?.message || t("app.action.error"), false);
}
}
+// -------------------------------------------------------
+// IMPORT
+// -------------------------------------------------------
+import { loadLanguage, translatePage, t } from "./i18n.js";
+
// -----------------------------
// DOMContentLoaded: initialize everything
// -----------------------------
-// Import common js
+// -------------------------------------------------------
+// IMPORT
+// -------------------------------------------------------
import { loadModals, showToast, showConfirmModal, handleReload } from './common.js';
-// Import services
import { serviceReloadDNS, serviceReloadDHCP, serviceRestartApp, serviceIsAlive, serviceGetLogs } from './services.js';
+import { loadLanguage, translatePage, t } from "./i18n.js";
+import { getBackendMessage } from "./backendMessages.js";
// -----------------------------
// State variables
updateFilterButton();
requestAnimationFrame(() => {
- logViewer.scrollTop = savedScrollPosition;
+ if (savedScrollPosition !== null)
+ logViewer.scrollTop = savedScrollPosition;
});
}
attempts++;
if (attempts > maxAttempts) {
clearInterval(interval);
- showToast("Server did not come back online", false);
+ showToast(t("app.reconnect.timeout"), false);
button.innerHTML = originalHtmlButton;
button.disabled = false;
return;
clearInterval(interval);
- showToast("Application is back online", true);
+ showToast(t("app.reconnect.success"), true);
setTimeout(() => location.reload(), 500);
}
} catch (err) {
- console.log("Waiting for server...");
+ console.log(t("app.reconnect.waiting"));
}
}, 2000); // check every 2 seconds
// Restart application
// -----------------------------
async function handleRestartApp(button) {
- const confirmed = await showConfirmModal("Restart the application?");
+ const confirmed = await showConfirmModal(t("app.restart.confirm"));
if (!confirmed) return;
const originalHtmlButton = button.innerHTML;
const ok = await handleReload(
button,
serviceRestartApp,
- "Application is restarting...",
- "Error restarting application",
- "Restarting...",
+ t("app.restart.progress"),
+ t("app.restart.error"),
+ t("app.restart.progress"),
true
);
//},
// Reload DNS
reloadDns: async (e, el) => {
- showToast("DNS is reloading...", true);
await handleReload(
el,
serviceReloadDNS,
- "DNS reload successfully",
- "Error reloading DNS",
- "Reloading DNS..."
+ t("dns.reload.ok"),
+ t("dns.reload.error"),
+ t("dns.reload.progress")
);
},
// Reload DHCP
reloadDhcp: async (e, el) => {
- showToast("DHCP is reloading...", true);
await handleReload(
el,
serviceReloadDHCP,
- "DHCP reload successfully",
- "Error reloading DHCP",
- "Reloading DHCP..."
+ t("dhcp.reload.ok"),
+ t("dhcp.reload.error"),
+ t("dhcp.reload.progress")
);
},
// Reload App
// DOMContentLoaded: bootstrap app
// -----------------------------
document.addEventListener("DOMContentLoaded", async () => {
- initApp();
- loadLogs();
+ await initApp();
+ await loadLogs();
});
// -----------------------------
// -----------------------------
async function initApp() {
+ // Loading translation
+ try {
+ await loadLanguage();
+ } catch (err) {
+ console.error(err?.message || t("app.translation.error"));
+ showToast(t("app.translation.error"), false);
+ }
+
// Load modals (Bootstrap 5 requires JS initialization for dynamic content)
try {
await loadModals();
} catch (err) {
- console.error(err?.message || "Error loading modals");
- showToast(err?.message || "Error loading modals", false);
+ console.error(err?.message || t("app.modals.error"));
+ showToast(t("app.modals.error"), false);
}
+ // Translate page
+ translatePage();
+
initEvents();
initDropdown();
initFilters();
try {
await handler(e, el);
} catch (err) {
- console.error(err?.message || 'Action error');
- showToast(err?.message || 'Action error', false);
+ console.error(err?.message || t("app.action.error"));
+ showToast(err?.message || t("app.action.error"), false);
}
}
// Reload DNS
// -----------------------------
export async function serviceReloadDNS() {
- const data = await apiPost(
+ return await apiPost(
"/api/dns/reload",
null,
"Error reloading DNS"
);
-
- return data?.message ? { message: data.message } : true;
}
// -----------------------------
// Reload DHCP action
// -----------------------------
export async function serviceReloadDHCP() {
- const data = await apiPost(
+ return await apiPost(
"/api/dhcp/reload",
null,
"Error reloading DHCP"
);
-
- return data?.message ? { message: data.message } : true;
}
// -----------------------------
// Delete DHCP Lease
// -----------------------------
export async function serviceDeleteDHCPLease(id) {
- const data = await apiRequest(
+ return await apiRequest(
`/api/dhcp/leases/${id}`,
{ method: "DELETE" },
"Error deleting host"
);
-
- return data?.message ? { message: data.message } : true;
}
// -----------------------------
// Create a new host
// -----------------------------
export async function serviceCreateHost(hostData) {
- const data = await apiRequest(
+ return await apiRequest(
"/api/hosts",
{
method: "POST",
},
"Error creating host"
);
-
- return data?.message ? { message: data.message } : true;
}
// -----------------------------
// Update an host
// -----------------------------
export async function serviceUpdateHost(id, hostData) {
- const data = await apiRequest(
+ return await apiRequest(
`/api/hosts/${id}`,
{
method: "PUT",
},
"Error updating host"
);
-
- return data?.message ? { message: data.message } : true;
}
// -----------------------------
// Delete Hosts
// -----------------------------
export async function serviceDeleteHost(id) {
- const data = await apiRequest(
+ return await apiRequest(
`/api/hosts/${id}`,
{ method: "DELETE" },
"Error deleting host"
);
-
- return data?.message ? { message: data.message } : true;
}
// -----------------------------
// Create a new alias
// -----------------------------
export async function serviceCreateAlias(aliasData) {
- const data = await apiRequest(
+ return await apiRequest(
"/api/aliases",
{
method: "POST",
},
"Error creating alias"
);
-
- return data?.message ? { message: data.message } : true;
}
// -----------------------------
// Update an alias
// -----------------------------
export async function serviceUpdateAlias(id, aliasData) {
- const data = await apiRequest(
+ return await apiRequest(
`/api/aliases/${id}`,
{
method: "PUT",
},
"Error updating alias"
);
-
- return data?.message ? { message: data.message } : true;
}
// -----------------------------
// Delete Alias
// -----------------------------
export async function serviceDeleteAlias(id) {
- const data = await apiRequest(
+ return await apiRequest(
`/api/aliases/${id}`,
{ method: "DELETE" },
"Error deleting alias"
);
-
- return data?.message ? { message: data.message } : true;
}
// -----------------------------
// Create a Backup
// -------------------------------------------------------
export async function serviceBackupCreate() {
- const data = await apiPost(
+ return await apiPost(
"/api/backup/create",
null,
"Error performing backup"
);
- if (data.status === 'success') {
- return data?.message ? { message: data.message } : true;
- }
-
- if (data.status === 'partial') {
- return data?.message
- ? { message: data.message, partial: true }
- : { partial: true };
- }
-
- return false;
}
// -------------------------------------------------------
// Restore a Backup
// -------------------------------------------------------
export async function serviceBackupRestore(id) {
- const data = await apiPost(
+ return await apiPost(
"/api/backup/restore",
{ backup_id: id },
"Error performing restore"
);
-
- if (data.status === 'success') {
- return data?.message ? { message: data.message } : true;
- }
-
- if (data.status === 'partial') {
- return data?.message
- ? { message: data.message, partial: true }
- : { partial: true };
- }
-
- return false;
}
// -------------------------------------------------------
// Delete a Backup
// -------------------------------------------------------
export async function serviceDeleteBackup(id) {
- const data = await apiPost(
+ return await apiPost(
"/api/backup/delete",
{ backup_id: id },
"Error performing delete"
);
-
- return data?.message ? { message: data.message } : true;
}
// -------------------------------------------------------
// Update a configuration parameter
// -----------------------------
export async function serviceUpdateConfig(key, configData) {
- const data = await apiRequest(
+ return await apiRequest(
`/api/settings/${key}`,
{
method: "PUT",
},
"Error updating configuration parameter"
);
-
- return data?.message ? { message: data.message } : true;
}
// -----------------------------
// Reset a configuration parameter to its default value
// -----------------------------
export async function serviceResetConfig(key) {
- const data = await apiPost(
+ return await apiPost(
`/api/settings/${key}/reset`,
null,
"Error restoring default value"
);
-
- return data?.message ? { message: data.message } : true;
}
// -----------------------------
// Reset a configuration parameter to its default value
// -----------------------------
export async function serviceRestartApp(key) {
- const data = await apiPost(
+ return await apiPost(
"/api/restart",
null,
"Error restarting application"
);
-
- return data?.message ? { message: data.message } : true;
}
// -----------------------------
-// Import common js
+// -------------------------------------------------------
+// IMPORT
+// -------------------------------------------------------
import { loadModals, showToast, handleSearch, clearSearch, showConfirmModal, handleReload } from './common.js';
-// Import services
import { serviceGetConfigs, serviceGetConfig, serviceUpdateConfig, serviceResetConfig, serviceReloadDNS, serviceReloadDHCP, serviceRestartApp, serviceIsAlive } from './services.js';
+import { loadLanguage, translatePage, t } from "./i18n.js";
+import { getBackendMessage } from "./backendMessages.js";
// -----------------------------
// State variables
viewConfigs = [...allConfigs];
} catch (err) {
- console.error(err?.message || "Error loading configs");
- showToast(err?.message || "Error loading configs", false);
+ console.error(err?.message || t("settings.list.error"));
+ showToast(err?.message || t("settings.list.error"), false);
allConfigs = [];
viewConfigs = [];
// hide loader and show table
attempts++;
if (attempts > maxAttempts) {
clearInterval(interval);
- showToast("Server did not come back online", false);
+ showToast(t("app.reconnect.timeout"), false);
button.innerHTML = originalHtmlButton;
button.disabled = false;
return;
clearInterval(interval);
- showToast("Application is back online", true);
+ showToast(t("app.reconnect.success"), true);
setTimeout(() => location.reload(), 500);
}
} catch (err) {
- console.log("Waiting for server...");
+ console.log(t("app.reconnect.waiting"));
}
}, 2000); // check every 2 seconds
// Restart application
// -----------------------------
async function handleRestartApp(button) {
- const confirmed = await showConfirmModal("Restart the application?");
+ const confirmed = await showConfirmModal(t("app.restart.confirm"));
if (!confirmed) return;
const originalHtmlButton = button.innerHTML;
const ok = await handleReload(
button,
serviceRestartApp,
- "Application is restarting...",
- "Error restarting application",
- "Restarting...",
+ t("app.restart.progress"),
+ t("app.restart.error"),
+ t("app.restart.progress"),
true
);
expandBtn?.toggleAttribute("disabled", hasSearch);
collapseBtn?.toggleAttribute("disabled", hasSearch);
if (hasSearch) {
- expandBtn?.setAttribute("title", "Disabled during search");
- collapseBtn?.setAttribute("title", "Disabled during search");
+ expandBtn?.setAttribute("title", t("settings.expand.disabled"));
+ collapseBtn?.setAttribute("title", t("settings.expand.disabled"));
} else {
- expandBtn?.setAttribute("title", "Expand all groups");
- collapseBtn?.setAttribute("title", "Collapse all groups");
+ expandBtn?.setAttribute("title", t("settings.expand.all"));
+ collapseBtn?.setAttribute("title", t("settings.collapse.all"));
}
// DOM Reference
const trEmpty = document.createElement("tr");
const tdEmpty = document.createElement("td");
tdEmpty.colSpan = 4;
- tdEmpty.textContent = "No configs available.";
+ tdEmpty.textContent = t("settings.empty");
tdEmpty.style.textAlign = "center";
trEmpty.appendChild(tdEmpty);
tbody.appendChild(trEmpty);
editSpan.className = "action-icon";
editSpan.setAttribute("role", "button");
editSpan.tabIndex = 0;
- editSpan.title = "Edit config";
- editSpan.setAttribute("aria-label", "Edit config");
+ const editText = t("settings.edit");
+ editSpan.title = editText;
+ editSpan.setAttribute("aria-label", editText);
editSpan.setAttribute("data-bs-toggle", "modal");
editSpan.setAttribute("data-bs-target", "#editConfigModal");
editSpan.setAttribute("data-action", "edit");
// Reset Button
const resetSpan = document.createElement("span");
+ const resetText = t("settings.reset");
resetSpan.className = "action-icon";
resetSpan.setAttribute("role", "button");
resetSpan.tabIndex = 0;
- resetSpan.title = "Reset to default value";
- resetSpan.setAttribute("aria-label", "Reset to default value");
+ resetSpan.title = resetText;
+ resetSpan.setAttribute("aria-label", resetText);
resetSpan.setAttribute("data-action", "reset");
resetSpan.setAttribute("data-config-key", String(c.key));
{
}
} catch (err) {
- console.error(err?.message || "Error loading config");
- showToast(err?.message || "Error loading config", false);
+ console.error(err?.message || t("settings.loaded.error"));
+ showToast(err?.message || t("settings.loaded.error"), false);
}
}
switch (typeof configData.value) {
case "string":
if (!configData.value.trim()) {
- showToast("Configuration value is required", false);
+ showToast(t("validation.config.required"), false);
return false;
}
break;
case "number":
if (isNaN(configData.value)) {
- showToast("Invalid numeric value", false);
+ showToast(t("validation.config.invalid_number"), false);
return false;
}
break;
default:
- showToast("Invalid configuration value", false);
+ showToast(t("validation.config.invalid"), false);
return false;
}
// Update
result = await serviceUpdateConfig(editingConfigKey, configData);
- const msg = (typeof result === 'object' && result?.message)
- ? result.message
- : 'Config updated successfully';
+ const msg = getBackendMessage(
+ result,
+ "settings.updated.ok"
+ );
showToast(msg, true);
return true;
} catch (err) {
- console.error(err?.message || "Error updating config");
- showToast(err?.message || "Error updating config", false);
+
+ const msg = getBackendMessage(
+ err,
+ "settings.updated.error"
+ );
+
+ showToast(msg, false);
}
return false;
closeEditConfigModal();
await fetchConfigs();
updateTable();
- return true
+ return true;
}
} catch (err) {
- console.error(err?.message || "Error saving config");
- showToast(err?.message || "Error saving config", false);
+ console.error(
+ err?.message ||
+ t("settings.updated.error")
+ );
+
+ showToast(
+ err?.message ||
+ t("settings.updated.error"),
+ false
+ );
}
return false;
// Get config ID
const key = el.dataset.configKey;
if (typeof key !== "string" || key.length === 0) {
- showToast('Configuration key not valid for reset', false);
+ showToast(t("settings.restored.invalid_id"), false);
return;
}
// Confirm requested
- const confirmed = await showConfirmModal("Reset this configuration?");
+ const confirmed = await showConfirmModal(t("settings.reset.confirm"));
if (!confirmed) return;
try {
const result = await serviceResetConfig(key);
- const msg = (typeof result === 'object' && result?.message)
- ? result.message
- : 'Config reset to default successfully';
-
+ const msg = getBackendMessage(result, "settings.restored.ok");
showToast(msg, true);
// Reload configs
return true;
} catch (err) {
- console.error(err?.message || "Error resetting config to default");
- showToast(err?.message || "Error resetting config to default", false);
+ console.error(err?.message || t("settings.restored.error"));
+ showToast(err?.message || t("settings.restored.error"), false);
}
return false;
// handled by bootstrap modal show event
},
reloadDns: async (e, el) => {
- showToast("DNS is reloading...", true);
await handleReload(
el,
serviceReloadDNS,
- "DNS reload successfully",
- "Error reloading DNS",
- "Reloading DNS..."
+ t("dns.reload.ok"),
+ t("dns.reload.error"),
+ t("dns.reload.progress")
);
},
// Reload DHCP
reloadDhcp: async (e, el) => {
- showToast("DHCP is reloading...", true);
await handleReload(
el,
serviceReloadDHCP,
- "DHCP reload successfully",
- "Error reloading DHCP",
- "Reloading DHCP..."
+ t("dhcp.reload.ok"),
+ t("dhcp.reload.error"),
+ t("dhcp.reload.progress")
);
},
// Reload App
// -----------------------------
async function initApp() {
+ // Loading translation
+ try {
+ await loadLanguage();
+ } catch (err) {
+ console.error(err?.message || t("app.translation.error"));
+ showToast(t("app.translation.error"), false);
+ }
+
// Load modals (Bootstrap 5 requires JS initialization for dynamic content)
try {
await loadModals();
} catch (err) {
- console.error(err?.message || "Error loading modals");
- showToast(err?.message || "Error loading modals", false);
+ console.error(err?.message || t("app.modals.error"));
+ showToast(t("app.modals.error"), false);
}
+ // Translate page
+ translatePage();
+
// Load data (configs)
try {
await fetchConfigs();
updateTable();
} catch (err) {
- console.error(err?.message || "Error loading configs");
- showToast(err?.message || "Error loading configs", false);
+ console.error(err?.message || t("settings.list.error"));
+ showToast(err?.message || t("settings.list.error"), false);
}
initUI();
try {
await editConfig(key);
} catch (err) {
- showToast(err?.message || "Error loading config", false);
+ showToast(err?.message || t("settings.loaded.error"), false);
// Close modal
modalEl.addEventListener('shown.bs.modal', () => {
closeEditConfigModal();
}, { once: true });
}
} else {
- console.warn("Invalid Configuration Key for edit");
closeEditConfigModal();
return;
}
const groupKey = groupRow.dataset.group;
toggleGroup(groupKey);
});
- document.getElementById("expandAllBtn")
- ?.addEventListener("click", expandAllGroups);
- document.getElementById("collapseAllBtn")
- ?.addEventListener("click", collapseAllGroups);
+ document.querySelectorAll(".expand-all-btn")
+ .forEach(btn => btn.addEventListener("click", expandAllGroups));
+ document.querySelectorAll(".collapse-all-btn")
+ .forEach(btn => btn.addEventListener("click", collapseAllGroups));
}
// -----------------------------
try {
await handler(e, el);
} catch (err) {
- console.error(err?.message || 'Action error');
- showToast(err?.message || 'Action error', false);
+ console.error(err?.message || t("app.action.error"));
+ showToast(err?.message || t("app.action.error"), false);
}
}
<link rel="stylesheet" href="css/layout.css">
</head>
-<body>
+<body data-page-title="dashboard.title">
<!-- Topbar -->
<header class="topbar">
<div class="topbar-inner">
<line x1="12" y1="6" x2="12" y2="18" stroke="var(--accent)" stroke-width="2"></line>
<line x1="6" y1="12" x2="18" y2="12" stroke="var(--accent)" stroke-width="2"></line>
</svg>
- <span>Network Manager</span>
+ <span data-i18n="app.name">Network Manager</span>
</a>
<nav class="navbar">
<!-- Desktop nav -->
<div class="d-none d-md-flex gap-2 ms-auto">
- <a href="/hosts" class="btn btn-primary"> Hosts</a>
- <a href="/aliases" class="btn btn-primary"> Alias</a>
- <a href="/leases" class="btn btn-primary active">DHCP Leases</a>
- <a href="/devices" class="btn btn-primary"> Devices</a>
- <a href="/logs" class="btn btn-primary"> Logs</a>
- <a href="/settings" class="btn btn-primary"> Settings</a>
- <button class="btn btn-primary btn-logout">Logout</button>
+ <a href="/hosts" class="btn btn-primary" data-i18n="hosts.title">Hosts</a>
+ <a href="/aliases" class="btn btn-primary" data-i18n="aliases.title">Alias</a>
+ <a href="/leases" class="btn btn-primary active" data-i18n="dhcp.title">DHCP Leases</a>
+ <a href="/devices" class="btn btn-primary" data-i18n="devices.title">Devices</a>
+ <a href="/logs" class="btn btn-primary" data-i18n="logs.title">Logs</a>
+ <a href="/settings" class="btn btn-primary" data-i18n="settings.title">Settings</a>
+ <button class="btn btn-primary btn-logout" data-i18n="auth.logout">Logout</button>
</div>
</nav>
<div class="offcanvas offcanvas-end d-md-none" tabindex="-1" id="mobileMenu">
<div class="offcanvas-header">
- <h5 class="offcanvas-title">Network Manager</h5>
+ <h5 class="offcanvas-title" data-i18n="app.name">Network Manager</h5>
<button
type="button"
class="btn-close"
+ data-i18n-title="common.close"
+ data-i18n-aria-label="common.close"
data-bs-dismiss="offcanvas">
</button>
</div>
<div class="offcanvas-body">
<div class="d-grid gap-2">
- <a href="/hosts" class="btn btn-primary"> Hosts</a>
- <a href="/aliases" class="btn btn-primary"> Alias</a>
- <a href="/leases" class="btn btn-primary active">DHCP Leases</a>
- <a href="/devices" class="btn btn-primary"> Devices</a>
- <a href="/logs" class="btn btn-primary"> Logs</a>
- <a href="/settings" class="btn btn-primary"> Settings</a>
- <button class="btn btn-primary btn-logout">Logout</button>
+ <a href="/hosts" class="btn btn-primary" data-i18n="hosts.title">Hosts</a>
+ <a href="/aliases" class="btn btn-primary" data-i18n="aliases.title">Alias</a>
+ <a href="/leases" class="btn btn-primary active" data-i18n="dhcp.title">DHCP Leases</a>
+ <a href="/devices" class="btn btn-primary" data-i18n="devices.title">Devices</a>
+ <a href="/logs" class="btn btn-primary" data-i18n="logs.title">Logs</a>
+ <a href="/settings" class="btn btn-primary" data-i18n="settings.title">Settings</a>
+ <button class="btn btn-primary btn-logout" data-i18n="auth.logout">Logout</button>
</div>
</div>
</div>
<div class="col-auto col-md-auto">
<h2 class="mb-0 d-flex align-items-center gap-2 lh-1">
<span class="title-icon"><i class="bi bi-broadcast"></i></span>
- <span class="section-title">DHCP Leases</span>
+ <span class="section-title" data-i18n="dhcp.page.title">DHCP Leases</span>
</h2>
</div>
<input
type="text"
id="searchInput"
- placeholder="Search..."
class="form-control form-control-sm placeholder-italic"
- aria-label="Search lease">
+ data-i18n-placeholder="common.search.placeholder"
+ data-i18n-aria-label="dhcp.leases.search">
</div>
</div>
<!-- Add host Desktop -->
<button
class="btn btn-primary d-none d-md-flex align-items-center gap-2 px-3"
- title="Add Host"
- aria-label="Add Host"
+ data-i18n-title="hosts.add"
+ data-i18n-aria-label="hosts.add"
data-bs-toggle="modal" data-bs-target="#addHostModal">
<i class="bi bi-plus-lg"></i>
- <span class="label">Add Host</span>
+ <span class="label" data-i18n="hosts.add">Add Host</span>
</button>
<!-- Separator -->
<!-- Reload DNS Desktop -->
<button
class="btn btn-primary d-none d-md-flex align-items-center gap-2 px-3"
- title="Reload DNS (BIND)"
- aria-label="Reload DNS"
+ data-i18n-title="dns.reload.description"
+ data-i18n-aria-label="dns.reload.description"
data-action="reloadDns">
<i class="bi bi-arrow-repeat"></i>
- <span class="label">Reload DNS</span>
+ <span class="label" data-i18n="dns.reload">Reload DNS</span>
</button>
<!-- Reload DHCP Desktop -->
<button
class="btn btn-primary d-none d-md-flex align-items-center gap-2 px-3"
- title="Reload DHCP (Kea)"
- aria-label="Reload DHCP"
+ data-i18n-title="dhcp.reload.description"
+ data-i18n-aria-label="dhcp.reload.description"
data-action="reloadDhcp">
<i class="bi bi-arrow-repeat"></i>
- <span class="label">Reload DHCP</span>
+ <span class="label" data-i18n="dhcp.reload">Reload DHCP</span>
</button>
<!-- Mobile Dropdown -->
<li class="px-2 py-1">
<button
class="btn btn-primary w-100 d-flex align-items-center gap-2"
- title="Add Host"
- aria-label="Add Host"
+ data-i18n-title="hosts.add"
+ data-i18n-aria-label="hosts.add"
data-bs-toggle="modal" data-bs-target="#addHostModal">
<i class="bi bi-plus-lg"></i>
- <span>Add Host</span>
+ <span data-i18n="hosts.add">Add Host</span>
</button>
</li>
<li class="px-2 py-1">
<button
class="btn btn-primary w-100 d-flex align-items-center gap-2"
- title="Reload DNS (BIND)"
- aria-label="Reload DNS"
+ data-i18n-title="dns.reload.description"
+ data-i18n-aria-label="dns.reload.description"
data-action="reloadDns">
<i class="bi bi-arrow-repeat"></i>
- <span>Reload DNS</span>
+ <span data-i18n="dns.reload">Reload DNS</span>
</button>
</li>
<li class="px-2 py-1">
<button
class="btn btn-primary w-100 d-flex align-items-center gap-2"
- title="Reload DHCP (Kea)"
- aria-label="Reload DHCP"
+ data-i18n-title="dhcp.reload.description"
+ data-i18n-aria-label="dhcp.reload.description"
data-action="reloadDhcp">
<i class="bi bi-arrow-repeat"></i>
- <span>Reload DHCP</span>
+ <span data-i18n="dhcp.reload">Reload DHCP</span>
</button>
</li>
</ul>
<input
type="text"
id="searchInputMobile"
- placeholder="Search..."
class="form-control form-control-sm placeholder-italic"
- aria-label="Search lease">
+ data-i18n-placeholder="common.search.placeholder"
+ data-i18n-aria-label="dhcp.lease.search">
</div>
</div>
</section>
<thead class="table-light">
<tr>
<th data-type="ipv4" data-sortable="true" data-sort="0">
- IP Address <span class="sort-arrow" aria-hidden="true"></span>
+ <span data-i18n="hosts.ipv4">IP Address</span>
+ <span class="sort-arrow" aria-hidden="true"></span>
</th>
<th data-type="mac" data-sortable="true" data-sort="1">
- MAC <span class="sort-arrow" aria-hidden="true"></span>
+ <span data-i18n="hosts.mac">MAC Address</span>
+ <span class="sort-arrow" aria-hidden="true"></span>
</th>
<th data-type="string" data-sortable="true" data-sort="2">
- Hostname <span class="sort-arrow" aria-hidden="true"></span>
+ <span data-i18n="hosts.hostname">Hostname</span>
+ <span class="sort-arrow" aria-hidden="true"></span>
</th>
<th data-type="string" data-sortable="true" data-sort="3">
- Start <span class="sort-arrow" aria-hidden="true"></span>
+ <span data-i18n="dhcp.lease.start">Start</span>
+ <span class="sort-arrow" aria-hidden="true"></span>
</th>
<th data-type="string" data-sortable="true" data-sort="4">
- End <span class="sort-arrow" aria-hidden="true"></span>
+ <span data-i18n="dhcp.lease.end">End</span>
+ <span class="sort-arrow" aria-hidden="true"></span>
</th>
- <th data-type="string" data-sortable="true" data-sort="5">
- State <span class="sort-arrow" aria-hidden="true"></span>
+ <th data-type="string" data-sortable="true" data-sort="5" class="text-center text-nowrap">
+ <span data-i18n="common.state">State</span>
+ <span class="sort-arrow" aria-hidden="true"></span>
</th>
<th data-type="string" data-sortable="false" class="text-center text-nowrap">
- Actions
+ <span data-i18n="common.actions">Actions</span>
</th>
</tr>
</thead>
<!-- Loader -->
<div id="loader" class="text-center my-3" style="display: none;">
<div class="spinner-border text-primary" role="status">
- <span class="visually-hidden">Loading...</span>
+ <span class="visually-hidden" data-i18n="common.loading">Loading...</span>
</div>
</div>
- <div id="devices-container"></div>
<!-- Modals -->
<div id="modals-container"></div>
--- /dev/null
+{
+ "language": "en",
+ "translations": {
+
+ "app.name": "Network Manager",
+ "app.translation.error": "Error loading translation",
+ "app.modals.error": "Error loading modals",
+ "app.action.error": "Action error",
+ "app.restart": "Restart",
+ "app.restart.description": "Restart application",
+ "app.restart.confirm": "Restart the application?",
+ "app.restart.progress": "Application restart in progress...",
+ "app.restart.error": "Error restarting application",
+ "app.reconnect.waiting": "Waiting for the server to come back online...",
+ "app.reconnect.success": "Application is back online",
+ "app.reconnect.timeout": "Server did not come back online",
+ "app.restart.button": "Restarting...",
+
+ "auth.login": "Login",
+ "auth.logout": "Logout",
+ "auth.username": "Username",
+ "auth.password": "Password",
+ "auth.sign_in": "Sign In",
+ "auth.session_expired": "Session expired",
+ "auth.invalid_credentials": "Invalid username or password",
+
+ "header.api_status": "API Status",
+ "header.api_status_online": "API OK",
+ "header.api_status_offline": "API OFFLINE",
+
+ "dashboard.title": "Dashboard",
+
+ "dns.title": "DNS",
+ "dns.card.title": "DNS (BIND)",
+ "dns.card.description": "Zones, records and configuration.",
+ "dns.config": "DNS Configuration",
+ "dns.reload": "Reload DNS",
+ "dns.reload.description": "Reload DNS (BIND)",
+ "dns.reload.ok": "DNS reloaded successfully",
+ "dns.reload.error":"Error reloading DNS",
+ "dns.reload.progress":"Restarting DNS...",
+
+ "dhcp.title": "DHCP Leases",
+ "dhcp.card.title": "DHCP (Kea)",
+ "dhcp.card.description": "Pools, leases and reservations.",
+ "dhcp.page.title": "DHCP Leases",
+ "dhcp.leases": "DHCP Leases",
+ "dhcp.leases.search": "Search DHCP Leases",
+ "dhcp.reload": "Reload DHCP",
+ "dhcp.reload.description": "Reload DHCP (Kea)",
+ "dhcp.lease.start": "Start",
+ "dhcp.lease.end": "End",
+ "dhcp.leases.empty": "No leases available",
+ "dhcp.leases.static": "static",
+ "dhcp.leases.static.description": "Static lease",
+ "dhcp.leases.active": "active",
+ "dhcp.leases.active.description": "Active lease",
+ "dhcp.leases.expired": "expired",
+ "dhcp.leases.expired.description": "Expired lease",
+ "dhcp.leases.released": "released",
+ "dhcp.leases.released.description": "Released lease",
+ "dhcp.leases.declined": "declined",
+ "dhcp.leases.declined.description": "Declined lease",
+ "dhcp.leases.add": "Add static lease",
+ "dhcp.leases.delete": "Delete lease",
+ "dhcp.reload.ok": "DHCP reloaded successfully",
+ "dhcp.reload.error":"Error reloading DHCP",
+ "dhcp.reload.progress":"Restarting DHCP...",
+ "dhcp.leases.deleted.ok": "Lease deleted successfully",
+ "dhcp.leases.deleted.error": "Failed to delete lease",
+ "dhcp.leases.list.error": "Failed to load leases",
+ "dhcp.leases.loaded.error": "Failed to load lease",
+ "dhcp.leases.delete.invalid_id": "Invalid lease ID for deletion",
+ "dhcp.leases.not.found": "Lease not found",
+
+ "hosts.title": "Hosts",
+ "hosts.card.title": "Hosts",
+ "hosts.card.description": "IP/MAC inventory.",
+ "hosts.page.title": "Host List",
+ "hosts.add": "Add Host",
+ "hosts.edit": "Edit Host",
+ "hosts.delete": "Delete Host",
+ "hosts.search": "Search Host",
+ "hosts.hostname": "Hostname",
+ "hosts.ipv4": "IPv4 Address",
+ "hosts.placeholder.ipv4": "e.g. 192.168.1.10",
+ "hosts.ipv6": "IPv6 Address",
+ "hosts.placeholder.ipv6": "e.g. fe80::1",
+ "hosts.mac": "MAC Address",
+ "hosts.placeholder.mac": "e.g. AA:BB:CC:DD:EE:FF",
+ "hosts.ssl": "SSL Certificate",
+ "hosts.ssl.enabled": "SSL Certificate enabled",
+ "hosts.visibility": "Visibility",
+ "hosts.visibility.local": "Local",
+ "hosts.visibility.local.description": "Only local (A record internally resolved)",
+ "hosts.visibility.global": "Global",
+ "hosts.visibility.global.description": "Internal and external are identical",
+ "hosts.visibility.alias": "Alias",
+ "hosts.visibility.alias.description": "External name is a CNAME to external_name",
+ "hosts.empty": "No hosts available.",
+ "hosts.created.ok": "Host created successfully",
+ "hosts.created.error": "Failed to create host",
+ "hosts.updated.ok": "Host updated successfully",
+ "hosts.updated.error": "Failed to update host",
+ "hosts.deleted.ok": "Host deleted successfully",
+ "hosts.deleted.error": "Failed to delete host",
+ "hosts.delete.invalid_id": "Invalid host ID for deletion",
+ "hosts.list.error": "Failed to load hosts",
+ "hosts.loaded.error": "Failed to load host",
+ "hosts.not.found": "Host not found",
+ "hosts.already.present": "Host already present",
+
+ "aliases.title": "Aliases",
+ "aliases.card.title": "Aliases",
+ "aliases.card.description": "DNS alias inventory.",
+ "aliases.page.title": "Alias List",
+ "aliases.add": "Add Alias",
+ "aliases.edit": "Edit Alias",
+ "aliases.delete": "Delete Alias",
+ "aliases.search": "Search Aliases",
+ "aliases.name": "Alias",
+ "aliases.target": "Target",
+ "aliases.ssl": "SSL Certificate",
+ "aliases.ssl.enabled": "SSL Certificate enabled",
+ "aliases.visibility": "Visibility",
+ "aliases.visibility.local": "Local",
+ "aliases.visibility.local.description": "Only local (CNAME record internally resolved)",
+ "aliases.visibility.global": "Global",
+ "aliases.visibility.global.description": "Internal and external are identical",
+ "aliases.visibility.alias": "Alias",
+ "aliases.visibility.alias.description": "External name is a CNAME to external_name",
+ "aliases.empty": "No aliases available.",
+ "aliases.created.ok": "Alias created successfully",
+ "aliases.created.error": "Failed to create alias",
+ "aliases.updated.ok": "Alias updated successfully",
+ "aliases.updated.error": "Failed to update alias",
+ "aliases.deleted.ok": "Alias deleted successfully",
+ "aliases.deleted.error": "Failed to delete alias",
+ "aliases.delete.invalid_id": "Invalid alias ID for deletion",
+ "aliases.list.error": "Failed to load aliases",
+ "aliases.loaded.error": "Failed to load alias",
+ "aliases.not.found": "Alias not found",
+ "aliases.already.present": "Alias already present",
+
+ "devices.title": "Devices",
+ "devices.card.title": "Devices",
+ "devices.card.description": "Device status (Host + DHCP).",
+ "devices.page.title": "Device List",
+ "devices.edit": "Edit Device",
+ "devices.delete": "Delete Device",
+ "devices.search": "Search Devices",
+ "devices.active": "Active",
+ "devices.active.description": "Device active",
+ "devices.not.active.description": "Device not active",
+ "devices.empty": "No devices available.",
+ "devices.created.ok": "Device created successfully",
+ "devices.created.error": "Failed to create device",
+ "devices.updated.ok": "Device updated successfully",
+ "devices.updated.error": "Failed to update device",
+ "devices.deleted.ok": "Device deleted successfully",
+ "devices.deleted.error": "Failed to delete device",
+ "devices.edit.invalid_id": "Invalid device ID for modification",
+ "devices.delete.invalid_id": "Invalid device ID for deletion",
+ "devices.list.error": "Failed to load devices",
+ "devices.loaded.error": "Failed to load device",
+ "devices.not.found": "Device not found",
+ "devices.already.present": "Device already present",
+ "devices.unknown.type": "Unknown device type",
+
+ "certificates.title": "Certificates",
+ "certificates.card.title": "Certificates",
+ "certificates.card.description": "Let's Encrypt certificates and renewals.",
+ "certificates.page.title": "Certificate List",
+ "certificates.empty": "No certificates available.",
+
+ "backup.title": "Backup",
+ "backup.card.title": "Backup & Restore",
+ "backup.card.description": "Backup execution and archive management.",
+ "backup.modal.title": "Backup Management",
+ "backup.create": "Create Backup",
+ "backup.create.progress": "Create\85",
+ "backup.manage": "Backup Management",
+ "backup.available": "Available backups",
+ "backup.name": "Name",
+ "backup.date": "Date",
+ "backup.size": "Size",
+ "backup.actions": "Actions",
+ "backup.loading": "Loading backups...",
+ "backup.restore": "Restore Backup",
+ "backup.restore.select": "Select one backup to restore.",
+ "backup.restore.progress": "Restore...",
+ "backup.upload": "Upload Backup",
+ "backup.upload.select": "Select a backup file to upload.",
+ "backup.upload.file": "Upload Backup File",
+ "backup.download.file": "Download Backup File",
+ "backup.delete.file": "Delete Backup File",
+ "backup.refresh": "Refresh List",
+ "backup.empty": "No backups available.",
+ "backup.create_ok": "Backup completed successfully",
+ "backup.create_partial": "Backup completed partially",
+ "backup.create_error": "Error performing backup",
+ "backup.restore_ok": "Restore completed successfully",
+ "backup.restore_partial": "Restore completed partially",
+ "backup.restore_error": "Error performing restore",
+ "backup.delete_ok": "Backup deleted successfully",
+ "backup.delete_error": "Error deleting backup",
+ "backup.download_ok": "Backup downloaded successfully",
+ "backup.download_error": "Error downloading backup",
+ "backup.upload_ok": "Backup uploaded successfully",
+ "backup.upload_error": "Error uploading backup",
+ "backup.refresh_ok": "Backup list refreshed successfully",
+ "backup.refresh_error": "Error refreshing backup list",
+ "backup.select": "Select a backup",
+ "backup.select_file": "Select a file first",
+ "backup.delete_confirm": "Delete backup \"{id}\"?",
+ "backup.not.found": "Backup not found",
+
+ "logs.title": "Logs",
+ "logs.card.title": "Logs",
+ "logs.card.description": "Events and access logs.",
+ "logs.page.title": "System Logs",
+ "logs.view": "View",
+ "logs.live": "Live",
+ "logs.live.description": "Live Stream On/Off",
+ "logs.refresh": "Refresh",
+ "logs.refresh.description": "Refresh logs",
+ "logs.app": "App",
+ "logs.search": "Search logs",
+ "logs.filter": "Filters",
+
+ "settings.title": "Settings",
+ "settings.card.title": "Settings",
+ "settings.card.description": "System configuration and variables.",
+ "settings.page.title": "Settings",
+ "settings.edit": "Edit setting",
+ "settings.reset": "Restore to default value",
+ "settings.reset.confirm": "Restore this setting to its default value?",
+ "settings.search": "Search Configuration Parameter",
+ "settings.parameter": "Parameter",
+ "settings.modal.parameter": "Configuration Parameter",
+ "settings.value": "Value",
+ "settings.enabled": "Enabled",
+ "settings.current": "Current",
+ "settings.admin": "Admin",
+ "settings.admin.description": "Admin menu",
+ "settings.empty": "No settings available",
+ "settings.updated.ok": "Setting updated successfully",
+ "settings.updated.error": "Failed to update setting",
+ "settings.restored.ok": "Setting restored to default values successfully",
+ "settings.restored.error": "Failed to restore default values",
+ "settings.restored.invalid_id": "Invalid setting key",
+ "settings.list.error": "Failed to load settings",
+ "settings.loaded.error": "Failed to load setting",
+ "settings.not.found": "Setting not found",
+ "settings.expand.disabled": "Disabled during search",
+ "settings.expand.all": "Expand all groups",
+ "settings.collapse.all": "Collapse all groups",
+
+ "health.title": "Health",
+ "health.card.title": "Health",
+ "health.card.description": "Service and resource status.",
+ "health.modal.title": "Health Status",
+ "health.loading": "Loading status...",
+ "health.error": "Error fetching health status.",
+ "health.update.ok": "API status updated succesfully",
+ "health.update.error": "Error updating API status",
+ "health.details_unavailable": "Health details not available",
+ "health.updated_at": "Updated at",
+ "health.status": "Status",
+ "health.status.healthy": "Healthy",
+ "health.status.degraded": "Degraded",
+ "health.status.unhealthy": "Unhealthy",
+ "health.status.unknown": "Unknown",
+ "health.latency": "Latency",
+ "health.db_status": "DB Status",
+ "health.db_version": "DB Version",
+ "health.db_tables": "DB Tables",
+ "health.db_size": "DB Size",
+
+ "common.close": "Close",
+ "common.cancel": "Cancel",
+ "common.confirm": "Confirm",
+ "common.confirmation": "Confirmation",
+ "common.are_you_sure": "Are you sure?",
+ "common.description": "Description",
+ "common.options": "Options",
+ "common.state": "State",
+ "common.actions": "Actions",
+ "common.loading": "Loading...",
+ "common.search": "Search",
+ "common.search.placeholder": "Search...",
+ "common.expand": "Expand",
+ "common.expand.description": "Expand all groups",
+ "common.collapse": "Collapse",
+ "common.collapse.description": "Collapse all groups",
+
+ "validation.name.required": "Hostname is required",
+ "validation.ipv4.invalid": "Invalid IPv4 format",
+ "validation.ipv6.invalid": "Invalid IPv6 format",
+ "validation.mac.invalid": "Invalid MAC format",
+ "validation.target.required": "Target is required",
+ "validation.alias.required": "Alias is required",
+ "validation.config.required": "Configuration value is required",
+ "validation.config.invalid_number": "Invalid numeric value",
+ "validation.config.invalid": "Invalid configuration value"
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "language": "it",
+ "translations": {
+
+ "app.name": "Network Manager",
+ "app.translation.error": "Errore durante il caricamento della traduzione",
+ "app.modals.error": "Errore nel caricamento delle finestre modali",
+ "app.action.error": "Errore nell'esecuzione di un'azione",
+ "app.restart": "Riavvia App",
+ "app.restart.description": "Riavvia applicazione",
+ "app.restart.confirm": "Riavviare l'applicazione?",
+ "app.restart.progress": "Riavvio applicazione in corso...",
+ "app.restart.error": "Errore durante il riavvio dell'applicazione",
+ "app.reconnect.waiting": "In attesa che il server torni disponibile...",
+ "app.reconnect.success": "Applicazione nuovamente disponibile",
+ "app.reconnect.timeout": "Il server non è tornato disponibile",
+ "app.restart.button": "Riavvio...",
+
+ "auth.login": "Accesso",
+ "auth.logout": "Logout",
+ "auth.username": "Nome utente",
+ "auth.password": "Password",
+ "auth.sign_in": "Accedi",
+ "auth.session_expired": "Sessione scaduta",
+ "auth.invalid_credentials": "Nome utente o password non validi",
+
+ "header.api_status": "Stato API",
+ "header.api_status_online": "API OK",
+ "header.api_status_offline": "API OFFLINE",
+
+ "dashboard.title": "Dashboard",
+
+ "dns.title": "DNS",
+ "dns.card.title": "DNS (BIND)",
+ "dns.card.description": "Zone, record e configurazioni.",
+ "dns.config": "Configurazione DNS",
+ "dns.reload": "Ricarica DNS",
+ "dns.reload.description": "Ricarica DNS (BIND)",
+ "dns.reload.ok": "DNS ricaricato con successo",
+ "dns.reload.error":"Errore nel ricaricare DNS",
+ "dns.reload.progress":"Riavvio DNS...",
+
+ "dhcp.title": "DHCP Leases",
+ "dhcp.card.title": "DHCP (Kea)",
+ "dhcp.card.description": "Pool, lease e prenotazioni.",
+ "dhcp.page.title": "Lease DHCP",
+ "dhcp.leases": "Lease DHCP",
+ "dhcp.leases.search": "Cerca Lease DHCP",
+ "dhcp.reload": "Ricarica DHCP",
+ "dhcp.reload.description": "Ricarica DHCP (Kea)",
+ "dhcp.lease.start": "Inizio",
+ "dhcp.lease.end": "Fine",
+ "dhcp.leases.empty": "Nessuna lease disponibile",
+ "dhcp.leases.static": "statico",
+ "dhcp.leases.static.description": "Lease statico",
+ "dhcp.leases.active": "attivo",
+ "dhcp.leases.active.description": "Lease attivo",
+ "dhcp.leases.expired": "scaduto",
+ "dhcp.leases.expired.description": "Lease scaduto",
+ "dhcp.leases.released": "rilasciato",
+ "dhcp.leases.released.description": "Lease rilasciato",
+ "dhcp.leases.declined": "rifiutato",
+ "dhcp.leases.declined.description": "Lease rifiutato",
+ "dhcp.leases.add": "Aggiungi lease statica",
+ "dhcp.leases.delete": "Cancella lease",
+ "dhcp.reload.ok": "DHCP ricaricato con successo",
+ "dhcp.reload.error":"Errore nel ricaricare DHCP",
+ "dhcp.reload.progress":"Riavvio DHCP...",
+ "dhcp.leases.deleted.ok": "Lease eliminata con successo",
+ "dhcp.leases.deleted.error": "Errore nell'eliminazione della lease",
+ "dhcp.leases.list.error": "Errore nel caricamento delle lease",
+ "dhcp.leases.loaded.error": "Errore nel caricamento della lease",
+ "dhcp.leases.delete.invalid_id": "ID lease non valido per la rimozione",
+ "dhcp.leases.not.found": "Lease non trovata",
+
+ "hosts.title": "Host",
+ "hosts.card.title": "Host",
+ "hosts.card.description": "Inventario IP/MAC.",
+ "hosts.page.title": "Lista Host",
+ "hosts.add": "Aggiungi Host",
+ "hosts.edit": "Modifica Host",
+ "hosts.delete": "Cancella Host",
+ "hosts.search": "Cerca Host",
+ "hosts.hostname": "Nome Host",
+ "hosts.ipv4": "Indirizzo IPv4",
+ "hosts.placeholder.ipv4": "es. 192.168.1.10",
+ "hosts.ipv6": "Indirizzo IPv6",
+ "hosts.placeholder.ipv6": "es. fe80::1",
+ "hosts.mac": "Indirizzo MAC",
+ "hosts.placeholder.mac": "es. AA:BB:CC:DD:EE:FF",
+ "hosts.ssl": "Certificato SSL",
+ "hosts.ssl.enabled": "Certificato SSL abilitato",
+ "hosts.visibility": "Visibilità",
+ "hosts.visibility.local": "Locale",
+ "hosts.visibility.local.description": "Solo locale (record A risolto internamente)",
+ "hosts.visibility.global": "Globale",
+ "hosts.visibility.global.description": "Il nome interno e quello esterno coincidono",
+ "hosts.visibility.alias": "Alias",
+ "hosts.visibility.alias.description": "Il nome esterno è un CNAME di external_name",
+ "hosts.empty": "Nessun host disponibile.",
+ "hosts.created.ok": "Host creato con successo",
+ "hosts.created.error": "Errore nel creare l'host",
+ "hosts.updated.ok": "Host modificato con successo",
+ "hosts.updated.error": "Errore nel modificare l'host",
+ "hosts.deleted.ok": "Host eliminato con successo",
+ "hosts.deleted.error": "Errore nell'eliminare l'host",
+ "hosts.delete.invalid_id": "ID host non valido per la rimozione",
+ "hosts.list.error": "Errore nel caricamento degli host",
+ "hosts.loaded.error": "Errore nel caricamento dell'host",
+ "hosts.not.found": "Host non trovato",
+ "hosts.already.present": "Host già presente",
+
+ "aliases.title": "Alias",
+ "aliases.card.title": "Alias",
+ "aliases.card.description": "Inventario alias DNS.",
+ "aliases.page.title": "Lista Alias",
+ "aliases.add": "Aggiungi Alias",
+ "aliases.edit": "Modifica Alias",
+ "aliases.delete": "Cancella Alias",
+ "aliases.search": "Cerca Alias",
+ "aliases.name": "Alias",
+ "aliases.target": "Target",
+ "aliases.ssl": "Certificato SSL",
+ "aliases.ssl.enabled": "Certificato SSL abilitato",
+ "aliases.visibility": "Visibilità",
+ "aliases.visibility.local": "Locale",
+ "aliases.visibility.local.description": "Solo locale (record CNAME risolto internamente)",
+ "aliases.visibility.global": "Globale",
+ "aliases.visibility.global.description": "Il nome interno e quello esterno coincidono",
+ "aliases.visibility.alias": "Alias",
+ "aliases.visibility.alias.description": "Il nome esterno è un CNAME di external_name",
+ "aliases.empty": "Nessun alias disponibile.",
+ "aliases.created.ok": "Alias creato con successo",
+ "aliases.created.error": "Errore nel creare l'alias",
+ "aliases.updated.ok": "Alias modificato con successo",
+ "aliases.updated.error": "Errore nel modificare l'alias",
+ "aliases.deleted.ok": "Alias eliminato con successo",
+ "aliases.deleted.error": "Errore nell'eliminare l'alias",
+ "aliases.delete.invalid_id": "ID alias non valido per la rimozione",
+ "aliases.list.error": "Errore nel caricamento degli alias",
+ "aliases.loaded.error": "Errore nel caricamento dell'alias",
+ "aliases.not.found": "Alias non trovato",
+ "aliases.already.present": "Alias già presente",
+
+ "devices.title": "Dispositivi",
+ "devices.card.title": "Dispositivi",
+ "devices.card.description": "Stato dispositivi (Host + DHCP).",
+ "devices.page.title": "Lista Dispositivi",
+ "devices.edit": "Modifica Dispositivo",
+ "devices.delete": "Cancella Dispositivo",
+ "devices.search": "Cerca Dispositivo",
+ "devices.active": "Attivo",
+ "devices.active.description": "Dispositivo attivo",
+ "devices.not.active.description": "Dispositivo non attivo",
+ "devices.empty": "Nessun dispositivo disponibile.",
+ "devices.created.ok": "Dispositivo creato con successo",
+ "devices.created.error": "Errore nel creare il dispositivo",
+ "devices.updated.ok": "Dispositivo modificato con successo",
+ "devices.updated.error": "Errore nel modificare il dispositivo",
+ "devices.deleted.ok": "Dispositivo eliminato con successo",
+ "devices.deleted.error": "Errore nell'eliminare il dispositivo",
+ "devices.edit.invalid_id": "ID dispositivo non valido per la modifica",
+ "devices.delete.invalid_id": "ID dispositivo non valido per la rimozione",
+ "devices.list.error": "Errore nel caricamento dei dispositivi",
+ "devices.loaded.error": "Errore nel caricamento del dispositivo",
+ "devices.not.found": "Dispositivo non trovato",
+ "devices.already.present": "Dispositivo già presente",
+ "devices.unknown.type": "Tipologia di dispositivo sconosciuta",
+
+ "certificates.title": "Certificati",
+ "certificates.card.title": "Certificati",
+ "certificates.card.description": "Certificati Let's Encrypt e rinnovi.",
+ "certificates.page.title": "Lista Certificati",
+ "certificates.empty": "Nessun certificato disponibile.",
+
+ "backup.title": "Backup",
+ "backup.card.title": "Backup e Ripristino",
+ "backup.card.description": "Esecuzione backup e gestione archivi.",
+ "backup.modal.title": "Gestione Backup",
+ "backup.create": "Crea Backup",
+ "backup.create.progress": "Creazione…",
+ "backup.manage": "Gestione Backup",
+ "backup.available": "Backup disponibili",
+ "backup.name": "Nome",
+ "backup.date": "Data",
+ "backup.size": "Dimensione",
+ "backup.actions": "Azioni",
+ "backup.loading": "Caricamento backup...",
+ "backup.restore": "Ripristina Backup",
+ "backup.restore.select": "Seleziona un backup da ripristinare.",
+ "backup.restore.progress": "Ripristino...",
+ "backup.upload": "Carica backup",
+ "backup.upload.select": "Seleziona un file di backup da caricare.",
+ "backup.upload.file": "Carica file backup",
+ "backup.download.file": "Scarica file backup",
+ "backup.delete.file": "Elimina file backup",
+ "backup.refresh": "Aggiorna elenco",
+ "backup.empty": "Nessun backup disponibile.",
+ "backup.create_ok": "Backup completato con successo",
+ "backup.create_partial": "Backup completato parzialmente",
+ "backup.create_error": "Errore durante il backup",
+ "backup.restore_ok": "Ripristino completato con successo",
+ "backup.restore_partial": "Ripristino completato parzialmente",
+ "backup.restore_error": "Errore durante il ripristino",
+ "backup.delete_ok": "Backup eliminato con successo",
+ "backup.delete_error": "Errore durante l'eliminazione del backup",
+ "backup.download_ok": "Backup scaricato con successo",
+ "backup.download_error": "Errore durante il download del backup",
+ "backup.upload_ok": "Backup caricato con successo",
+ "backup.upload_error": "Errore durante il caricamento del backup",
+ "backup.refresh_ok": "Lista backup aggiornata con successo",
+ "backup.refresh_error": "Errore durante l'aggiornamento della lista backup",
+ "backup.select": "Seleziona un backup",
+ "backup.select_file": "Seleziona prima un file",
+ "backup.delete_confirm": "Eliminare il backup \"{id}\"?",
+ "backup.not.found": "Backup non trovato",
+
+ "logs.title": "Log",
+ "logs.card.title": "Log",
+ "logs.card.description": "Eventi e log di accesso.",
+ "logs.page.title": "Log di sistema",
+ "logs.view": "Vista",
+ "logs.live": "Live",
+ "logs.live.description": "Attiva/Disattiva Live Stream",
+ "logs.refresh": "Ricarica",
+ "logs.refresh.description": "Ricarica log",
+ "logs.app": "App",
+ "logs.search": "Cerca log",
+ "logs.filter": "Filtri",
+
+ "settings.title": "Impostazioni",
+ "settings.card.title": "Impostazioni",
+ "settings.card.description": "Configurazione sistema e variabili.",
+ "settings.page.title": "Impostazioni",
+ "settings.edit": "Modifica parametro",
+ "settings.reset": "Ripristina al valore predefinito",
+ "settings.reset.confirm": "Ripristinare questo parametro ai valori predefiniti?",
+ "settings.search": "Cerca parametro",
+ "settings.parameter": "Parametro",
+ "settings.modal.parameter": "Parametro di configurazione",
+ "settings.value": "Valore",
+ "settings.enabled": "Abilitato",
+ "settings.current": "Corrente",
+ "settings.admin": "Admin",
+ "settings.admin.description": "Menu amministrazione",
+ "settings.empty": "Nessun parametro disponibile",
+ "settings.updated.ok": "Parametro modificato con successo",
+ "settings.updated.error": "Errore nel modificare il parametro",
+ "settings.restored.ok": "Parametro ripristinato ai valori predefiniti",
+ "settings.restored.error": "Errore durante il ripristino dei valori predefiniti",
+ "settings.restored.invalid_id": "Chiave parametro non valida",
+ "settings.list.error": "Errore nel caricamento dei parametri",
+ "settings.loaded.error": "Errore nel caricamento del parametro",
+ "settings.not.found": "Parametro non trovato",
+ "settings.expand.disabled": "Disabilitato durante la ricerca",
+ "settings.expand.all": "Espandi tutti i gruppi",
+ "settings.collapse.all": "Comprimi tutti i gruppi",
+
+ "health.title": "Stato del sistema",
+ "health.card.title": "Stato del sistema",
+ "health.card.description": "Stato servizi e risorse.",
+ "health.modal.title": "Stato del sistema",
+ "health.loading": "Caricamento stato...",
+ "health.error": "Errore durante il recupero dello stato del sistema.",
+ "health.update.ok": "Stato API aggiornato con successo",
+ "health.update.error": "Errore nell'aggiornamento dello stato API",
+ "health.details_unavailable": "Informazioni sullo stato del sistema non disponibili",
+ "health.updated_at": "Aggiornato alle",
+ "health.status": "Stato",
+ "health.status.healthy": "Normale",
+ "health.status.degraded": "Degradato",
+ "health.status.unhealthy": "Critico",
+ "health.status.unknown": "Sconosciuto",
+ "health.latency": "Latenza",
+ "health.db_status": "Stato DB",
+ "health.db_version": "Versione DB",
+ "health.db_tables": "Tabelle DB",
+ "health.db_size": "Dimensione DB",
+
+ "common.close": "Chiudi",
+ "common.cancel": "Annulla",
+ "common.confirm": "Conferma",
+ "common.confirmation": "Conferma",
+ "common.are_you_sure": "Sei sicuro?",
+ "common.description": "Descrizione",
+ "common.options": "Opzioni",
+ "common.state": "Stato",
+ "common.actions": "Azioni",
+ "common.loading": "Caricamento...",
+ "common.search": "Cerca",
+ "common.search.placeholder": "Cerca...",
+ "common.expand": "Espandi",
+ "common.expand.description": "Espandi tutti i gruppi",
+ "common.collapse": "Comprimi",
+ "common.collapse.description": "Comprimi tutti i gruppi",
+
+ "validation.name.required": "Nome host richiesto",
+ "validation.ipv4.invalid": "Formato IPv4 non valido",
+ "validation.ipv6.invalid": "Formato IPv6 non valido",
+ "validation.mac.invalid": "Formato MAC non valido",
+ "validation.target.required": "Target richiesto",
+ "validation.alias.required": "Alias richiesto",
+ "validation.config.required": "Valore del parametro obbligatorio",
+ "validation.config.invalid_number": "Valore numerico non valido",
+ "validation.config.invalid": "Valore del parametro non valido"
+ }
+}
\ No newline at end of file
<link rel="stylesheet" href="css/layout.css">
</head>
-<body>
+<body data-page-title="dashboard.title">
<!-- Topbar -->
<header class="topbar">
<div class="topbar-inner">
<line x1="12" y1="6" x2="12" y2="18" stroke="var(--accent)" stroke-width="2"></line>
<line x1="6" y1="12" x2="18" y2="12" stroke="var(--accent)" stroke-width="2"></line>
</svg>
- <span>Network Manager</span>
+ <span data-i18n="app.name">Network Manager</span>
</a>
<nav class="navbar">
<!-- Desktop nav -->
<div class="d-none d-md-flex gap-2 ms-auto">
- <a href="/hosts" class="btn btn-primary"> Hosts</a>
- <a href="/aliases" class="btn btn-primary"> Alias</a>
- <a href="/leases" class="btn btn-primary"> DHCP Leases</a>
- <a href="/devices" class="btn btn-primary"> Devices</a>
- <a href="/logs" class="btn btn-primary active">Logs</a>
- <a href="/settings" class="btn btn-primary"> Settings</a>
- <button class="btn btn-primary btn-logout">Logout</button>
+ <a href="/hosts" class="btn btn-primary" data-i18n="hosts.title">Hosts</a>
+ <a href="/aliases" class="btn btn-primary" data-i18n="aliases.title">Alias</a>
+ <a href="/leases" class="btn btn-primary" data-i18n="dhcp.title">DHCP Leases</a>
+ <a href="/devices" class="btn btn-primary" data-i18n="devices.title">Devices</a>
+ <a href="/logs" class="btn btn-primary active" data-i18n="logs.title">Logs</a>
+ <a href="/settings" class="btn btn-primary" data-i18n="settings.title">Settings</a>
+ <button class="btn btn-primary btn-logout" data-i18n="auth.logout">Logout</button>
</div>
</nav>
<div class="offcanvas offcanvas-end d-md-none" tabindex="-1" id="mobileMenu">
<div class="offcanvas-header">
- <h5 class="offcanvas-title">Network Manager</h5>
+ <h5 class="offcanvas-title" data-i18n="app.name">Network Manager</h5>
<button
type="button"
class="btn-close"
+ data-i18n-title="common.close"
+ data-i18n-aria-label="common.close"
data-bs-dismiss="offcanvas">
</button>
</div>
<div class="offcanvas-body">
<div class="d-grid gap-2">
- <a href="/hosts" class="btn btn-primary"> Hosts</a>
- <a href="/aliases" class="btn btn-primary"> Alias</a>
- <a href="/leases" class="btn btn-primary"> DHCP Leases</a>
- <a href="/devices" class="btn btn-primary"> Devices</a>
- <a href="/logs" class="btn btn-primary active">Logs</a>
- <a href="/settings" class="btn btn-primary"> Settings</a>
- <button class="btn btn-primary btn-logout">Logout</button>
+ <a href="/hosts" class="btn btn-primary" data-i18n="hosts.title">Hosts</a>
+ <a href="/aliases" class="btn btn-primary" data-i18n="aliases.title">Alias</a>
+ <a href="/leases" class="btn btn-primary" data-i18n="dhcp.title">DHCP Leases</a>
+ <a href="/devices" class="btn btn-primary" data-i18n="devices.title">Devices</a>
+ <a href="/logs" class="btn btn-primary active" data-i18n="logs.title">Logs</a>
+ <a href="/settings" class="btn btn-primary" data-i18n="settings.title">Settings</a>
+ <button class="btn btn-primary btn-logout" data-i18n="auth.logout">Logout</button>
</div>
</div>
</div>
<div class="col-auto col-md-auto">
<h2 class="mb-0 d-flex align-items-center gap-2 lh-1">
<span class="title-icon"><i class="bi bi-hdd-network"></i></span>
- <span class="section-title">System Logs</span>
+ <span class="section-title" data-i18n="logs.page.title">System Logs</span>
</h2>
</div>
class="btn btn-primary dropdown-toggle d-flex align-items-center gap-2"
data-bs-toggle="dropdown">
<i class="bi bi-eye"></i>
- <span class="label d-none d-md-inline">View</span>
+ <span class="label d-none d-md-inline" data-i18n="logs.view">View</span>
</button>
<ul class="dropdown-menu dropdown-menu-end shadow">
<li class="px-2 py-1">
<button
class="btn btn-success w-100 d-flex align-items-center gap-2"
- title="Live Stream On"
- aria-label="Live Stream On"
+ data-i18n-title="logs.live.description"
+ data-i18n-aria-label="logs.live.description"
data-action="liveToggle">
<i class="bi bi-broadcast"></i>
- <span class="label d-md-inline">Live</span>
+ <span class="label d-md-inline" data-i18n="logs.live">Live</span>
</button>
</li>
<li class="px-2 py-1">
<button
class="btn btn-primary w-100 d-flex align-items-center gap-2"
- title="Refresh"
- aria-label="Refresh"
+ data-i18n-title="logs.refresh"
+ data-i18n-aria-label="logs.refresh"
data-action="refreshLogs">
<i class="bi bi-arrow-repeat"></i>
- <span class="label d-md-inline">Refresh</span>
+ <span class="label d-md-inline" data-i18n="logs.refresh">Refresh</span>
</button>
</li>
</ul>
class="btn btn-primary dropdown-toggle d-flex align-items-center gap-2 px-3"
data-bs-toggle="dropdown">
<i class="bi bi-file-text"></i>
- <span class="label d-none d-md-inline">App</span>
+ <span class="label d-none d-md-inline" data-i18n="logs.app">App</span>
</button>
<ul class="dropdown-menu dropdown-menu-end shadow">
<li><a class="dropdown-item active" data-value="app" href="#">App</a></li>
class="btn btn-primary dropdown-toggle d-flex align-items-center gap-2 px-3"
data-bs-toggle="dropdown">
<i class="bi bi-sliders"></i>
- <span class="label d-none d-md-inline">Filter</span>
+ <span class="label d-none d-md-inline" data-i18n="logs.filter">Filter</span>
</button>
<div class="dropdown-menu dropdown-menu-end shadow p-3" id="filterDropdown">
<!-- Search Bar -->
<div class="mb-2">
- <label class="form-label small">Search</label>
+ <label class="form-label small" data-i18n="common.search">Search</label>
<input
type="text"
id="logSearch"
class="form-control mb-2"
- placeholder="Search logs">
+ data-i18n-placeholder="common.search.placeholder"
+ data-i18n-aria-label="logs.search">
</div>
<!-- Separator -->
<li class="px-2 py-1">
<button
class="btn btn-primary w-100 d-flex align-items-center gap-2 text-nowrap"
- title="Reload DNS (BIND)"
- aria-label="Reload DNS"
+ data-i18n-title="dns.reload.description"
+ data-i18n-aria-label="dns.reload.description"
data-action="reloadDns">
<i class="bi bi-arrow-clockwise"></i>
- <span class="label d-md-inline">Reload DNS</span>
+ <span class="label d-md-inline" data-i18n="dns.reload">Reload DNS</span>
</button>
</li>
<li class="px-2 py-1">
<button
class="btn btn-primary w-100 d-flex align-items-center gap-2 text-nowrap"
- title="Reload DHCP (Kea)"
- aria-label="Reload DHCP"
+ data-i18n-title="dhcp.reload.description"
+ data-i18n-aria-label="dhcp.reload.description"
data-action="reloadDhcp">
<i class="bi bi-arrow-clockwise"></i>
- <span class="label d-md-inline">Reload DHCP</span>
+ <span class="label d-md-inline" data-i18n="dhcp.reload">Reload DHCP</span>
</button>
</li>
<li class="px-2 py-1">
<button
class="btn btn-danger w-100 d-flex align-items-center gap-2 text-nowrap"
- title="Restart system"
- aria-label="Restart system"
+ data-i18n-title="app.restart.description"
+ data-i18n-aria-label="app.restart.description"
data-action="restartApp">
<i class="bi bi-power"></i>
- <span class="label d-md-inline">Restart</span>
+ <span class="label d-md-inline" data-i18n="app.restart">Restart</span>
</button>
</li>
</ul>
<!-- Loader -->
<div id="loader" class="text-center my-3" style="display: none;">
<div class="spinner-border text-primary" role="status">
- <span class="visually-hidden">Loading...</span>
+ <span class="visually-hidden" data-i18n="common.loading">Loading...</span>
</div>
</div>
- <div id="devices-container"></div>
<!-- Modals -->
<div id="modals-container"></div>
-<!-- AddHost -->
+<!-- Modal hosts -->
<div class="modal fade" id="addHostModal" tabindex="-1" aria-labelledby="addHostTitle" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered"><!-- modal-sm|md|lg se vuoi cambiare -->
<div class="modal-content modal-content-sm addhost-modal">
<div class="d-flex align-items-center gap-2">
<!-- Emoji o icona -->
<span class="title-icon" aria-hidden="true"><i class="bi bi-hdd-network"></i></span>
- <h5 class="modal-title mb-0" id="addHostTitle">Aggiungi Host</h5>
+ <h5 class="modal-title mb-0" id="addHostTitle" data-i18n="hosts.add">Add host</h5>
</div>
- <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Chiudi"></button>
+ <button type="button"
+ class="btn-close btn-close-white"
+ data-bs-dismiss="modal"
+ data-i18n-aria-label="common.close"></button>
</div>
<div class="modal-body">
<form id="addHostForm">
<div class="mb-2">
- <label for="hostName" class="form-label">Hostname</label>
+ <label for="hostName" class="form-label" data-i18n="hosts.hostname">Hostname</label>
<input type="text" id="hostName" class="form-control" required>
</div>
<div class="mb-2">
- <label for="hostIPv4" class="form-label">IPv4</label>
- <input type="text" id="hostIPv4" class="form-control" inputmode="decimal" placeholder="es. 192.168.1.10">
+ <label for="hostIPv4" class="form-label" data-i18n="hosts.ipv4">IPv4</label>
+ <input type="text" id="hostIPv4" class="form-control" inputmode="decimal" data-i18n-placeholder="hosts.placeholder.ipv4">
</div>
<div class="mb-2">
- <label for="hostIPv6" class="form-label">IPv6</label>
- <input type="text" id="hostIPv6" class="form-control" placeholder="es. fe80::1">
+ <label for="hostIPv6" class="form-label" data-i18n="hosts.ipv6">IPv6</label>
+ <input type="text" id="hostIPv6" class="form-control" data-i18n-placeholder="hosts.placeholder.ipv6">
</div>
<div class="mb-2">
- <label for="hostMAC" class="form-label">MAC Address</label>
- <input type="text" id="hostMAC" class="form-control" placeholder="es. AA:BB:CC:DD:EE:FF">
+ <label for="hostMAC" class="form-label" data-i18n="hosts.mac">MAC Address</label>
+ <input type="text" id="hostMAC" class="form-control" data-i18n-placeholder="hosts.placeholder.mac">
</div>
<div class="mb-2">
- <label for="hostDescription" class="form-label">Description</label>
+ <label for="hostDescription" class="form-label" data-i18n="common.description">Description</label>
<input type="text" id="hostDescription" class="form-control">
</div>
<div class="form-check my-2">
<input class="form-check-input" type="checkbox" id="hostSSL">
- <label class="form-check-label" for="hostSSL">SSL?</label>
+ <label class="form-check-label" for="hostSSL" data-i18n="hosts.ssl">SSL?</label>
</div>
<div class="mb-2">
- <label class="form-label d-block">Visibility</label>
+ <label class="form-label d-block" data-i18n="hosts.visibility">Visibility</label>
<div class="btn-group" role="group">
<!-- Local -->
<input type="radio" class="btn-check" id="hostVisibilityLocal" name="hostVisibility" value="0" checked>
- <label class="btn btn-outline-primary" for="hostVisibilityLocal">Local</label>
+ <label class="btn btn-outline-primary" for="hostVisibilityLocal" data-i18n="hosts.visibility.local">Local</label>
<!-- Global -->
<input type="radio" class="btn-check" id="hostVisibilityGlobal" name="hostVisibility" value="1">
- <label class="btn btn-outline-primary" for="hostVisibilityGlobal">Global</label>
+ <label class="btn btn-outline-primary" for="hostVisibilityGlobal" data-i18n="hosts.visibility.global">Global</label>
<!-- Alias -->
<input type="radio" class="btn-check" id="hostVisibilityAlias" name="hostVisibility" value="2">
- <label class="btn btn-outline-primary" for="hostVisibilityAlias">Alias</label>
+ <label class="btn btn-outline-primary" for="hostVisibilityAlias" data-i18n="hosts.visibility.alias">Alias</label>
</div>
</div>
</form>
</div>
<div class="modal-footer">
- <button type="submit" form="addHostForm" class="btn btn-primary">
- <i class="bi bi-check2"></i>
+ <button type="submit"
+ form="addHostForm"
+ class="btn btn-primary"
+ data-i18n-title="common.confirm"
+ data-i18n-aria-label="common.confirm">
+ <i class="bi bi-check2"></i>
</button>
- <button type="button" class="btn btn-primary" data-bs-dismiss="modal">
- <i class="bi bi-x"></i>
+ <button type="button"
+ class="btn btn-primary"
+ data-bs-dismiss="modal"
+ data-i18n-title="common.cancel"
+ data-i18n-aria-label="common.cancel">
+ <i class="bi bi-x"></i>
</button>
</div>
</div>
</div>
</div>
-<!-- AddAlias -->
+<!-- Modal aliases -->
<div class="modal fade" id="addAliasModal" tabindex="-1" aria-labelledby="addAliasTitle" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered"><!-- modal-sm|md|lg se vuoi cambiare -->
<div class="modal-content modal-content-sm addhost-modal">
<div class="d-flex align-items-center gap-2">
<!-- Emoji o icona -->
<span class="title-icon" aria-hidden="true"><i class="bi bi-diagram-2"></i></span>
- <h5 class="modal-title mb-0" id="addAliasTitle">Aggiungi Alias</h5>
+ <h5 class="modal-title mb-0" id="addAliasTitle" data-i18n="aliases.add">Add alias</h5>
</div>
- <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Chiudi"></button>
+ <button type="button"
+ class="btn-close btn-close-white"
+ data-bs-dismiss="modal"
+ data-i18n-aria-label="common.close">
+ </button>
</div>
<div class="modal-body">
<form id="addAliasForm">
<div class="mb-2">
- <label for="aliasName" class="form-label">Alias</label>
+ <label for="aliasName" class="form-label" data-i18n="aliases.name">Alias</label>
<input type="text" id="aliasName" class="form-control" required>
</div>
<div class="mb-2">
- <label for="aliasTarget" class="form-label">Target</label>
+ <label for="aliasTarget" class="form-label" data-i18n="aliases.target">Target</label>
<input type="text" id="aliasTarget" class="form-control" required>
</div>
<div class="mb-2">
- <label for="aliasDescription" class="form-label">Description</label>
+ <label for="aliasDescription" class="form-label" data-i18n="common.description">Description</label>
<input type="text" id="aliasDescription" class="form-control">
</div>
<div class="form-check my-2">
<input class="form-check-input" type="checkbox" id="aliasSSL">
- <label class="form-check-label" for="aliasSSL">SSL?</label>
+ <label class="form-check-label" for="aliasSSL" data-i18n="aliases.ssl">SSL?</label>
</div>
<div class="mb-2">
- <label class="form-label d-block">Visibility</label>
+ <label class="form-label d-block" data-i18n="aliases.visibility">Visibility</label>
<div class="btn-group" role="group">
<!-- Local -->
<input type="radio" class="btn-check" id="aliasVisibilityLocal" name="aliasVisibility" value="0" checked>
- <label class="btn btn-outline-primary" for="aliasVisibilityLocal">Local</label>
+ <label class="btn btn-outline-primary" for="aliasVisibilityLocal" data-i18n="aliases.visibility.local">Local</label>
<!-- Global -->
<input type="radio" class="btn-check" id="aliasVisibilityGlobal" name="aliasVisibility" value="1">
- <label class="btn btn-outline-primary" for="aliasVisibilityGlobal">Global</label>
+ <label class="btn btn-outline-primary" for="aliasVisibilityGlobal" data-i18n="aliases.visibility.global">Global</label>
<!-- Alias -->
<input type="radio" class="btn-check" id="aliasVisibilityAlias" name="aliasVisibility" value="2">
- <label class="btn btn-outline-primary" for="aliasVisibilityAlias">Alias</label>
+ <label class="btn btn-outline-primary" for="aliasVisibilityAlias" data-i18n="aliases.visibility.alias">Alias</label>
</div>
</div>
</form>
</div>
<div class="modal-footer">
- <button type="submit" form="addAliasForm" class="btn btn-primary">
- <i class="bi bi-check2"></i>
+ <button type="submit"
+ form="addAliasForm"
+ class="btn btn-primary"
+ data-i18n-title="common.confirm"
+ data-i18n-aria-label="common.confirm">
+ <i class="bi bi-check2"></i>
</button>
- <button type="button" class="btn btn-primary" data-bs-dismiss="modal">
- <i class="bi bi-x"></i>
+ <button type="button"
+ class="btn btn-primary"
+ data-bs-dismiss="modal"
+ data-i18n-title="common.cancel"
+ data-i18n-aria-label="common.cancel">
+ <i class="bi bi-x"></i>
</button>
</div>
</div>
<div class="addhost-header d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center gap-2">
<span class="title-icon"><i class="bi bi-arrow-counterclockwise"></i></span>
- <span id="backupTitle" class="modal-title">Backup Management</span>
+ <span id="backupTitle" class="modal-title" data-i18n="backup.modal.title">Backup Management</span>
</div>
- <button type="button" class="btn-close" title="Close" aria-label="Close" data-action="closeBackupModal"></button>
+ <button type="button"
+ class="btn-close"
+ data-i18n-title="common.close"
+ data-i18n-aria-label="common.close"
+ data-action="closeBackupModal">
+ </button>
</div>
<div class="modal-body">
- <label class="form-label">Available backups</label>
+ <label class="form-label" data-i18n="backup.available">Available backups</label>
<div class="table-responsive" style="max-height: 250px; overflow-y: auto;">
<table class="table table-hover table-sm align-middle">
<thead class="table-light sticky-top">
<tr>
<th style="width: 40px;"></th>
- <th>Name</th>
- <th>Date</th>
- <th>Size</th>
- <th style="width: 130px;" class="text-center">Actions</th>
+ <th data-i18n="backup.name">Name</th>
+ <th data-i18n="backup.date">Date</th>
+ <th data-i18n="backup.size">Size</th>
+ <th style="width: 130px;"
+ class="text-center"
+ data-i18n="backup.actions">
+ Actions
+ </th>
</tr>
</thead>
<tbody id="backupList">
<!-- Hidden input opzionale -->
<input type="hidden" id="restoreBackupId" />
- <small class="text-muted">Select one backup to restore.</small>
+ <small class="text-muted" data-i18n="backup.restore.select">Select one backup to restore.</small>
</div>
<div class="modal-body">
- <label class="form-label">Upload backup</label>
+ <label class="form-label" data-i18n="backup.upload">Upload backup</label>
<div class="d-flex gap-2">
<input type="file" id="backupUploadInput" class="form-control" accept=".zip,.bak,.tar,.gz">
- <button type="button" class="btn btn-primary" title="Upload backup file" aria-label="Upload backup file" data-action="uploadBackup">
- <i class="bi bi-upload icon"></i><span class="label"></span>
+ <button type="button"
+ class="btn btn-primary"
+ data-i18n-title="backup.upload.file"
+ data-i18n-aria-label="backup.upload.file"
+ data-action="uploadBackup">
+ <i class="bi bi-upload icon"></i><span class="label"></span>
</button>
</div>
- <small class="text-muted">Select a backup file to upload.</small>
+ <small class="text-muted" data-i18n="backup.upload.select">Select a backup file to upload.</small>
</div>
<div class="modal-footer modal-buttons">
- <button type="button" class="btn btn-primary" data-action="startBackup">
- <i class="bi bi-cloud-upload me-1"></i><span class="label">Create Backup</span>
+ <button type="button"
+ class="btn btn-primary"
+ data-action="startBackup">
+ <i class="bi bi-cloud-upload me-1"></i>
+ <span class="label"
+ data-i18n-title="backup.create"
+ data-i18n-aria-label="backup.create"
+ data-i18n="backup.create">
+ Create Backup
+ </span>
</button>
- <button type="button" class="btn btn-primary" data-action="startRestore">
- <i class="bi bi-cloud-download me-1"></i><span class="label">Restore Restore</span>
+ <button type="button"
+ class="btn btn-primary"
+ data-action="startRestore">
+ <i class="bi bi-cloud-download me-1"></i>
+ <span class="label"
+ data-i18n-title="backup.restore"
+ data-i18n-aria-label="backup.restore"
+ data-i18n="backup.restore">
+ Restore Backup
+ </span>
</button>
- <button type="button" class="btn btn-primary" data-action="refreshBackupList">
- <i class="bi bi-arrow-clockwise me-1"></i><span class="label">Refresh List</span>
+ <button type="button"
+ class="btn btn-primary"
+ data-action="refreshBackupList">
+ <i class="bi bi-arrow-clockwise me-1"></i>
+ <span class="label"
+ data-i18n-title="backup.refresh"
+ data-i18n-aria-label="backup.refresh"
+ data-i18n="backup.refresh">
+ Refresh List
+ </span>
</button>
- <button type="button" class="btn btn-outline-primary" data-action="closeBackupModal">
- <i class="bi bi-x-circle me-1"></i><span class="label">Cancel</span>
+ <button type="button"
+ class="btn btn-outline-primary"
+ data-action="closeBackupModal">
+ <i class="bi bi-x-circle me-1"></i>
+ <span class="label"
+ data-i18n-title="common.close"
+ data-i18n-aria-label="common.close"
+ data-i18n="common.close">
+ Cancel
+ </span>
</button>
</div>
</div>
</div>
-<!-- Modal Health -->
+<!-- Modal health -->
<div id="healthModal" class="modal" role="dialog" aria-modal="true" aria-labelledby="healthTitle">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content modal-content-md addhost-modal">
<div class="addhost-header d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center gap-2">
<span class="title-icon"><i class="bi bi-heart-pulse"></i></span>
- <span id="healthTitle" class="modal-title">Health Status</span>
+ <span id="healthTitle" class="modal-title" data-i18n="health.modal.title">Health Status</span>
</div>
- <button type="button" class="btn-close" title="Close" aria-label="Close" data-action="closeHealthModal"></button>
+ <button type="button"
+ class="btn-close"
+ data-i18n-title="common.close"
+ data-i18n-aria-label="common.close"
+ data-action="closeHealthModal">
+ </button>
</div>
<div class="modal-body">
<!-- Loading -->
<div id="healthLoading" class="d-flex align-items-center gap-2">
- <div class="spinner-border text-primary spinner-border-sm" role="status" aria-label="Loading"></div>
- <span class="text-muted">Loading status…</span>
+ <div class="spinner-border text-primary spinner-border-sm" role="status" data-i18n-aria-label="health.loading"></div>
+ <span class="text-muted" data-i18n="health.loading">Loading status…</span>
</div>
<!-- Content -->
</div>
<!-- Error -->
- <div id="healthError" class="alert alert-danger d-none" role="alert" aria-live="assertive">
+ <div id="healthError" class="alert alert-danger d-none" role="alert" aria-live="assertive" data-i18n="health.error">
Error fetching health status. Please try again later.
</div>
</div>
<div class="modal-footer modal-buttons">
- <button type="button" class="btn btn-outline-primary" data-action="closeHealthModal">
- <i class="bi bi-x-circle me-1"></i><span class="label">Close</span>
+ <button type="button"
+ class="btn btn-outline-primary"
+ data-i18n-title="common.close"
+ data-i18n-aria-label="common.close"
+ data-action="closeHealthModal">
+ <i class="bi bi-x-circle me-1"></i><span class="label" data-i18n="common.close">Close</span>
</button>
</div>
</div>
</div>
</div>
-<!-- EditConfig -->
+<!-- Modal settings -->
<div class="modal fade" id="editConfigModal" tabindex="-1" aria-labelledby="editConfigTitle" aria-hidden="true">
- <div class="modal-dialog modal-dialog-centered"><!-- modal-sm|md|lg se vuoi cambiare -->
+ <div class="modal-dialog modal-dialog-centered">
<div class="modal-content modal-content-sm addhost-modal">
- <!-- Header scuro con logo/brand -->
+ <!-- Dark Header with logo -->
<div class="modal-header addhost-header">
<div class="d-flex align-items-center gap-2">
<!-- Emoji o icona -->
<span class="title-icon" aria-hidden="true"><i class="bi bi-diagram-2"></i></span>
- <h5 class="modal-title mb-0" id="editConfigTitle">Modifica Parametro</h5>
+ <h5 class="modal-title mb-0" id="editConfigTitle" data-i18n="settings.edit.title">Edit Configuration</h5>
</div>
- <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Chiudi"></button>
+ <button type="button"
+ class="btn-close btn-close-white"
+ data-bs-dismiss="modal"
+ data-i18n-title="common.close"
+ data-i18n-aria-label="common.close">
+ </button>
</div>
<div class="modal-body">
<form id="editConfigForm">
<div class="mb-2">
- <label for="configKey" class="form-label">Configuration Parameter</label>
+ <label for="configKey" class="form-label" data-i18n="settings.modal.parameter">Configuration Parameter</label>
<input type="text" id="configKey" class="form-control" readonly>
</div>
<div class="mb-2">
- <label for="configDescription" class="form-label">Description</label>
+ <label for="configDescription" class="form-label" data-i18n="common.description">Description</label>
<input type="text" id="configDescription" class="form-control" readonly>
</div>
<!-- Boolean Input -->
<div class="mb-2 d-none" id="configValueBoolGroup">
- <label class="form-label">Value</label>
+ <label class="form-label" data-i18n="settings.value">Value</label>
<div class="form-check form-switch">
<input type="checkbox" id="configValueBool" class="form-check-input">
- <label class="form-check-label" for="configValueBool">Enabled</label>
+ <label class="form-check-label" for="configValueBool" data-i18n="settings.enabled">Enabled</label>
</div>
</div>
<!-- Range Input -->
<div class="mb-2 d-none" id="configValueRangeGroup">
- <label for="configValueRange" class="form-label">Value</label>
+ <label for="configValueRange" class="form-label" data-i18n="settings.value">Value</label>
<input type="range" id="configValueRange" class="form-range">
<div class="small text-muted">
- Current: <span id="configValueRangeDisplay"></span>
+ <span data-i18n="settings.current">Current</span>:
+ <span id="configValueRangeDisplay"></span>
</div>
</div>
<!-- Select Input -->
<div class="mb-2 d-none" id="configValueSelectGroup">
- <label for="configValueSelect" class="form-label">Value</label>
+ <label for="configValueSelect" class="form-label" data-i18n="settings.value">Value</label>
<select id="configValueSelect" class="form-select"></select>
</div>
<!-- Text Input -->
<div class="mb-2 d-none" id="configValueGroup">
- <label for="configValue" class="form-label">Value</label>
+ <label for="configValue" class="form-label" data-i18n="settings.value">Value</label>
<input type="text" id="configValue" class="form-control">
</div>
</form>
</div>
<div class="modal-footer">
- <button type="submit" form="editConfigForm" class="btn btn-primary">
- <i class="bi bi-check2"></i>
+ <button type="submit"
+ form="editConfigForm"
+ class="btn btn-primary"
+ data-i18n-title="common.confirm"
+ data-i18n-aria-label="common.confirm">
+ <i class="bi bi-check2"></i>
</button>
- <button type="button" class="btn btn-primary" data-bs-dismiss="modal">
- <i class="bi bi-x"></i>
+ <button type="button"
+ class="btn btn-primary"
+ data-bs-dismiss="modal"
+ data-i18n-title="common.cancel"
+ data-i18n-aria-label="common.cancel">
+ <i class="bi bi-x"></i>
</button>
</div>
</div>
<div class="modal-content">
<div class="modal-header">
- <h5 class="modal-title">Confirmation</h5>
- <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
+ <h5 class="modal-title" data-i18n="common.confirmation">Confirmation</h5>
+ <button type="button"
+ class="btn-close"
+ data-i18n-title="common.close"
+ data-i18n-aria-label="common.close"
+ data-bs-dismiss="modal">
+ </button>
</div>
- <div class="modal-body" id="confirmModalBody">
+ <div class="modal-body" id="confirmModalBody" data-i18n="common.are_you_sure">
Are you sure?
</div>
<div class="modal-footer">
- <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
- Cancel
+ <button type="button"
+ class="btn btn-secondary"
+ data-i18n-title="common.cancel"
+ data-i18n-aria-label="common.cancel"
+ data-bs-dismiss="modal"
+ data-i18n="common.cancel">
+ Cancel
</button>
- <button type="button" class="btn btn-danger" id="confirmModalOk">
- Confirm
+ <button type="button"
+ class="btn btn-danger"
+ data-i18n-title="common.confirm"
+ data-i18n-aria-label="common.confirm"
+ id="confirmModalOk"
+ data-i18n="common.confirm">
+ Confirm
</button>
</div>
<link rel="stylesheet" href="css/layout.css">
</head>
-<body>
+<body data-page-title="dashboard.title">
<!-- Topbar -->
<header class="topbar">
<div class="topbar-inner">
<line x1="12" y1="6" x2="12" y2="18" stroke="var(--accent)" stroke-width="2"></line>
<line x1="6" y1="12" x2="18" y2="12" stroke="var(--accent)" stroke-width="2"></line>
</svg>
- <span>Network Manager</span>
+ <span data-i18n="app.name">Network Manager</span>
</a>
<nav class="navbar">
<!-- Desktop nav -->
<div class="d-none d-md-flex gap-2 ms-auto">
- <a href="/hosts" class="btn btn-primary"> Hosts</a>
- <a href="/aliases" class="btn btn-primary"> Alias</a>
- <a href="/leases" class="btn btn-primary"> DHCP Leases</a>
- <a href="/devices" class="btn btn-primary"> Devices</a>
- <a href="/logs" class="btn btn-primary"> Logs</a>
- <a href="/settings" class="btn btn-primary active">Settings</a>
- <button class="btn btn-primary btn-logout">Logout</button>
+ <a href="/hosts" class="btn btn-primary" data-i18n="hosts.title">Hosts</a>
+ <a href="/aliases" class="btn btn-primary" data-i18n="aliases.title">Alias</a>
+ <a href="/leases" class="btn btn-primary" data-i18n="dhcp.title">DHCP Leases</a>
+ <a href="/devices" class="btn btn-primary" data-i18n="devices.title">Devices</a>
+ <a href="/logs" class="btn btn-primary" data-i18n="logs.title">Logs</a>
+ <a href="/settings" class="btn btn-primary active" data-i18n="settings.title">Settings</a>
+ <button class="btn btn-primary btn-logout" data-i18n="auth.logout">Logout</button>
</div>
</nav>
<div class="offcanvas offcanvas-end d-md-none" tabindex="-1" id="mobileMenu">
<div class="offcanvas-header">
- <h5 class="offcanvas-title">Network Manager</h5>
+ <h5 class="offcanvas-title" data-i18n="app.name">Network Manager</h5>
<button
type="button"
class="btn-close"
+ data-i18n-title="common.close"
+ data-i18n-aria-label="common.close"
data-bs-dismiss="offcanvas">
</button>
</div>
<div class="offcanvas-body">
<div class="d-grid gap-2">
- <a href="/hosts" class="btn btn-primary"> Hosts</a>
- <a href="/aliases" class="btn btn-primary"> Alias</a>
- <a href="/leases" class="btn btn-primary"> DHCP Leases</a>
- <a href="/devices" class="btn btn-primary"> Devices</a>
- <a href="/logs" class="btn btn-primary"> Logs</a>
- <a href="/settings" class="btn btn-primary active">Settings</a>
- <button class="btn btn-primary btn-logout">Logout</button>
+ <a href="/hosts" class="btn btn-primary" data-i18n="hosts.title">Hosts</a>
+ <a href="/aliases" class="btn btn-primary" data-i18n="aliases.title">Alias</a>
+ <a href="/leases" class="btn btn-primary" data-i18n="dhcp.title">DHCP Leases</a>
+ <a href="/devices" class="btn btn-primary" data-i18n="devices.title">Devices</a>
+ <a href="/logs" class="btn btn-primary" data-i18n="logs.title">Logs</a>
+ <a href="/settings" class="btn btn-primary active" data-i18n="settings.title">Settings</a>
+ <button class="btn btn-primary btn-logout" data-i18n="auth.logout">Logout</button>
</div>
</div>
</div>
<div class="col-auto col-md-auto">
<h2 class="mb-0 d-flex align-items-center gap-2 lh-1">
<span class="title-icon"><i class="bi bi-gear"></i></span>
- <span class="section-title">Settings</span>
+ <span class="section-title" data-i18n="settings.page.title">Settings</span>
</h2>
</div>
<input
type="text"
id="searchInput"
- placeholder="Search..."
class="form-control form-control-sm placeholder-italic"
- aria-label="Search config">
+ data-i18n-placeholder="common.search.placeholder"
+ data-i18n-aria-label="settings.search">
</div>
</div>
<!-- Separator -->
<div class="vr mx-2 d-none d-md-block"></div>
- <!-- Expand -->
+ <!-- Expand Desktop -->
<button
- class="btn btn-primary d-none d-md-flex align-items-center gap-2 px-3"
- title="Expand all groups"
- aria-label="Expand all groups"
- id="expandAllBtn">
+ class="btn btn-primary expand-all-btn d-none d-md-flex align-items-center gap-2 px-3"
+ data-i18n-title="common.expand.description"
+ data-i18n-aria-label="common.expand.description">
<i class="bi bi-arrows-expand"></i>
- <span class="label">Expand</span>
+ <span class="label" data-i18n="common.expand">Expand</span>
</button>
- <!-- Collapse -->
+ <!-- Collapse Desktop -->
<button
- class="btn btn-primary d-none d-md-flex align-items-center gap-2 px-3"
- title="Collapse all groups"
- aria-label="Collapse all groups"
- id="collapseAllBtn">
+ class="btn btn-primary collapse-all-btn d-none d-md-flex align-items-center gap-2 px-3"
+ data-i18n-title="common.collapse.description"
+ data-i18n-aria-label="common.collapse.description">
<i class="bi bi-arrows-collapse"></i>
- <span class="label">Collapse</span>
+ <span class="label" data-i18n="common.collapse">Collapse</span>
</button>
<!-- Separator -->
<!-- Admin Panel -->
<div id="adminDropdown" class="dropdown">
<button
- id="adminBtn"
class="btn btn-primary dropdown-toggle d-none d-md-flex align-items-center gap-2 px-3"
- data-bs-toggle="dropdown">
+ data-i18n-title="settings.admin.description"
+ data-i18n-aria-label="settings.admin.description"
+ data-bs-toggle="dropdown"
+ id="adminBtn">
<i class="bi bi-gear"></i>
- <span class="label d-none d-md-inline">Admin</span>
+ <span class="label d-none d-md-inline" data-i18n="settings.admin">Admin</span>
</button>
<ul class="dropdown-menu dropdown-menu-end shadow">
- <!-- Reload DNS -->
+ <!-- Reload DNS Desktop -->
<li class="px-2 py-1">
<button
class="btn btn-primary w-100 d-flex align-items-center gap-2 text-nowrap"
- title="Reload DNS (BIND)"
- aria-label="Reload DNS"
+ data-i18n-title="dns.reload.description"
+ data-i18n-aria-label="dns.reload.description"
data-action="reloadDns">
<i class="bi bi-arrow-clockwise"></i>
- <span class="label d-md-inline">Reload DNS</span>
+ <span class="label d-md-inline" data-i18n="dns.reload">Reload DNS</span>
</button>
</li>
- <!-- Reload DHCP -->
+ <!-- Reload DHCP Desktop -->
<li class="px-2 py-1">
<button
class="btn btn-primary w-100 d-flex align-items-center gap-2 text-nowrap"
- title="Reload DHCP (Kea)"
- aria-label="Reload DHCP"
+ data-i18n-title="dhcp.reload.description"
+ data-i18n-aria-label="dhcp.reload.description"
data-action="reloadDhcp">
<i class="bi bi-arrow-clockwise"></i>
- <span class="label d-md-inline">Reload DHCP</span>
+ <span class="label d-md-inline" data-i18n="dhcp.reload">Reload DHCP</span>
</button>
</li>
<li class="px-2 py-1">
<button
class="btn btn-danger w-100 d-flex align-items-center gap-2 text-nowrap"
- title="Restart system"
- aria-label="Restart system"
+ data-i18n-title="app.restart.description"
+ data-i18n-aria-label="app.restart.description"
data-action="restartApp">
<i class="bi bi-power"></i>
- <span class="label d-md-inline">Restart</span>
+ <span class="label d-md-inline" data-i18n="app.restart">Restart</span>
</button>
</li>
</ul>
<!-- Expand Mobile -->
<li class="px-2 py-1">
<button
- class="btn btn-primary w-100 d-flex align-items-center gap-2"
- title="Expand all groups"
- aria-label="Expand all groups"
- id="expandAllBtn">
+ class="btn btn-primary expand-all-btn w-100 d-flex align-items-center gap-2"
+ data-i18n-title="common.expand.description"
+ data-i18n-aria-label="common.expand.description">
<i class="bi bi-arrows-expand"></i>
- <span class="label">Expand</span>
+ <span class="label" data-i18n="common.expand">Expand</span>
</button>
</li>
<!-- Collapse Mobile -->
<li class="px-2 py-1">
<button
- class="btn btn-primary w-100 d-flex align-items-center gap-2"
- title="Collapse all groups"
- aria-label="Collapse all groups"
- id="collapseAllBtn">
+ class="btn btn-primary collapse-all-btn w-100 d-flex align-items-center gap-2"
+ data-i18n-title="common.collapse.description"
+ data-i18n-aria-label="common.collapse.description">
<i class="bi bi-arrows-collapse"></i>
- <span class="label">Collapse</span>
+ <span class="label" data-i18n="common.collapse">Collapse</span>
</button>
</li>
<li class="px-2 py-1">
<button
class="btn btn-primary w-100 d-flex align-items-center gap-2"
- title="Reload DNS (BIND)"
- aria-label="Reload DNS"
+ data-i18n-title="dns.reload.description"
+ data-i18n-aria-label="dns.reload.description"
data-action="reloadDns">
- <i class="bi bi-arrow-clockwise"></i>
- <span class="label d-md-inline">Reload DNS</span>
+ <i class="bi bi-arrow-repeat"></i>
+ <span data-i18n="dns.reload">Reload DNS</span>
</button>
</li>
<li class="px-2 py-1">
<button
class="btn btn-primary w-100 d-flex align-items-center gap-2"
- title="Reload DHCP (Kea)"
- aria-label="Reload DHCP"
+ data-i18n-title="dhcp.reload.description"
+ data-i18n-aria-label="dhcp.reload.description"
data-action="reloadDhcp">
<i class="bi bi-arrow-clockwise"></i>
- <span class="label d-md-inline">Reload DHCP</span>
+ <span class="label d-md-inline" data-i18n="dhcp.reload">Reload DHCP</span>
</button>
</li>
<li class="px-2 py-1">
<button
class="btn btn-danger w-100 d-flex align-items-center gap-2"
- title="Restart system"
- aria-label="Restart system"
+ data-i18n-title="app.reload.description"
+ data-i18n-aria-label="app.reload.description"
data-action="restartApp">
<i class="bi bi-arrow-repeat"></i>
- <span class="label">Restart</span>
+ <span class="label" data-i18n="app.reload">Restart</span>
</button>
</li>
</ul>
<input
type="text"
id="searchInputMobile"
- placeholder="Search..."
class="form-control form-control-sm placeholder-italic"
- aria-label="Search config">
+ data-i18n-placeholder="common.search.placeholder"
+ data-i18n-aria-label="settings.search">
</div>
</div>
</section>
<thead class="table-light">
<tr>
<th data-type="string" data-sortable="false">
- Parameter <span class="sort-arrow" aria-hidden="true"></span>
+ <span data-i18n="settings.parameter">Parameter</span>
</th>
<th data-type="string" data-sortable="false">
- Value <span class="sort-arrow" aria-hidden="true"></span>
+ <span data-i18n="settings.value">Value</span>
</th>
<th data-type="string" data-sortable="false">
- Description <span class="sort-arrow" aria-hidden="true"></span>
+ <span data-i18n="common.description">Description</span>
</th>
<th data-type="string" data-sortable="false" class="text-center text-nowrap">
- Actions
+ <span data-i18n="common.actions">Actions</span>
</th>
</tr>
</thead>
<!-- Loader -->
<div id="loader" class="text-center my-3" style="display: none;">
<div class="spinner-border text-primary" role="status">
- <span class="visually-hidden">Loading...</span>
+ <span class="visually-hidden" data-i18n="common.loading">Loading...</span>
</div>
</div>
- <div id="devices-container"></div>
<!-- Modals -->
<div id="modals-container"></div>