From 9bfc42c2cf008ee8d5b32331e8b3a141f0cbae28 Mon Sep 17 00:00:00 2001 From: Giorgio Ravera Date: Wed, 5 Aug 2026 20:43:48 +0200 Subject: [PATCH] Added translation --- TODO.md | 2 +- backend/app.py | 2 + backend/backup.py | 94 ++++++---- backend/bootstrap.py | 4 + backend/db/aliases.py | 3 +- backend/db/hosts.py | 23 ++- backend/db/settings.py | 7 + backend/routes/about.py | 43 ----- backend/routes/aliases.py | 144 +++++++-------- backend/routes/backup.py | 79 ++++---- backend/routes/devices.py | 40 ++-- backend/routes/dhcp.py | 18 +- backend/routes/dns.py | 4 +- backend/routes/health.py | 4 +- backend/routes/hosts.py | 145 +++++++-------- backend/routes/localization.py | 87 +++++++++ backend/routes/settings.py | 70 ++++--- backend/routes/system.py | 12 +- backend/settings/default.py | 5 + backend/settings/localization.py | 41 +++++ backend/settings/settings.py | 3 + frontend/aliases.html | 99 +++++----- frontend/css/layout.css | 2 +- frontend/devices.html | 105 ++++++----- frontend/hosts.html | 102 +++++----- frontend/index.html | 84 ++++----- frontend/js/aliases.js | 177 +++++++++++------- frontend/js/api.js | 14 +- frontend/js/backendMessages.js | 80 ++++++++ frontend/js/common.js | 45 ++--- frontend/js/devices.js | 185 +++++++++++-------- frontend/js/hosts.js | 164 ++++++++++------- frontend/js/i18n.js | 72 ++++++++ frontend/js/index.js | 244 ++++++++++++++++-------- frontend/js/leases.js | 123 +++++++------ frontend/js/login.js | 5 + frontend/js/logs.js | 61 +++--- frontend/js/services.js | 79 ++------ frontend/js/settings.js | 146 +++++++++------ frontend/leases.html | 107 ++++++----- frontend/locales/en.json | 307 +++++++++++++++++++++++++++++++ frontend/locales/it.json | 307 +++++++++++++++++++++++++++++++ frontend/logs.html | 82 +++++---- frontend/modals.html | 280 +++++++++++++++++++--------- frontend/settings.html | 147 ++++++++------- 45 files changed, 2539 insertions(+), 1308 deletions(-) delete mode 100644 backend/routes/about.py create mode 100644 backend/routes/localization.py create mode 100644 backend/settings/localization.py create mode 100644 frontend/js/backendMessages.js create mode 100644 frontend/js/i18n.js create mode 100644 frontend/locales/en.json create mode 100644 frontend/locales/it.json diff --git a/TODO.md b/TODO.md index 80061a9..490eca0 100644 --- a/TODO.md +++ b/TODO.md @@ -145,7 +145,7 @@ - [ ] Backup of generated configurations ### 🌍 Language -- [ ] Localization +- [X] Localization --- diff --git a/backend/app.py b/backend/app.py index 9d19221..889607f 100644 --- a/backend/app.py +++ b/backend/app.py @@ -21,6 +21,7 @@ from backend.routes.certificates import router as certificates_router 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 @@ -226,6 +227,7 @@ def create_app() -> FastAPI: app.include_router(backup_router) app.include_router(logs_router) app.include_router(settings_router) + app.include_router(localization_router) # CORS cors_origins = [ diff --git a/backend/backup.py b/backend/backup.py index bba95e8..053fc33 100644 --- a/backend/backup.py +++ b/backend/backup.py @@ -4,8 +4,8 @@ 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 @@ -33,6 +33,15 @@ backup_files = [ # 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 # --------------------------------------------------------- @@ -53,6 +62,7 @@ def build_result(operations: Dict[str, Dict[str, Any]]) -> Dict[str, Any]: "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( @@ -281,7 +291,7 @@ def restore_hosts( 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 @@ -293,12 +303,11 @@ def restore_hosts( "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, } @@ -399,7 +408,7 @@ def restore_aliases( 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 @@ -411,12 +420,11 @@ def restore_aliases( "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, } @@ -574,6 +582,14 @@ def check_metadata( # --------------------------------------------------------- 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) @@ -581,21 +597,13 @@ def backup_create() -> Dict[str, Any]: # 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) @@ -632,7 +640,7 @@ def backup_list() -> List[Dict[str, Any]]: # --------------------------------------------------------- def backup_restore(backup_id: str, cleanup: bool = True) -> Dict[str, Any]: - # Init struttura unica + # Initialization operations = { "archive": {}, "metadata": {}, @@ -647,7 +655,11 @@ def backup_restore(backup_id: str, cleanup: bool = True) -> Dict[str, Any]: 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) @@ -657,6 +669,7 @@ def backup_restore(backup_id: str, cleanup: bool = True) -> Dict[str, Any]: # --- 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 --- @@ -664,9 +677,17 @@ def backup_restore(backup_id: str, cleanup: bool = True) -> Dict[str, Any]: 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"]: @@ -677,9 +698,7 @@ def backup_restore(backup_id: str, cleanup: bool = True) -> Dict[str, Any]: 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) @@ -689,8 +708,10 @@ def backup_restore(backup_id: str, cleanup: bool = True) -> Dict[str, Any]: 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 @@ -698,20 +719,29 @@ def backup_delete(backup_id: str) -> Dict[str, Any]: 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) diff --git a/backend/bootstrap.py b/backend/bootstrap.py index a098680..f869fb1 100644 --- a/backend/bootstrap.py +++ b/backend/bootstrap.py @@ -36,6 +36,10 @@ def print_welcome(logger): "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) diff --git a/backend/db/aliases.py b/backend/db/aliases.py index 73465ef..a1745b8 100644 --- a/backend/db/aliases.py +++ b/backend/db/aliases.py @@ -82,7 +82,7 @@ def get_alias(alias_id: int) -> Optional[Dict[str, Any]]: # ----------------------------- # ADD ALIAS # ----------------------------- -def add_alias(data: Dict[str, Any]) -> int: +def add_alias(data: Dict[str, Any]): # Validate input cleaned = validate_data(data) @@ -103,7 +103,6 @@ def add_alias(data: Dict[str, Any]) -> int: ), ) conn.commit() - return cur.lastrowid except sqlite3.IntegrityError: conn.rollback() diff --git a/backend/db/hosts.py b/backend/db/hosts.py index 3498df5..bae2ece 100644 --- a/backend/db/hosts.py +++ b/backend/db/hosts.py @@ -53,13 +53,13 @@ def validate_data(data: Dict[str, Any]) -> Dict[str, Any]: 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 @@ -128,7 +128,7 @@ def get_host(host_id: int) -> Optional[Dict[str, Any]]: # ----------------------------- # ADD HOST # ----------------------------- -def add_host(data: Dict[str, Any]) -> int: +def add_host(data: Dict[str, Any]): # Validate input cleaned = validate_data(data) @@ -151,7 +151,6 @@ def add_host(data: Dict[str, Any]) -> int: ), ) conn.commit() - return cur.lastrowid except sqlite3.IntegrityError: conn.rollback() @@ -165,7 +164,7 @@ def add_host(data: Dict[str, Any]) -> int: # ----------------------------- # 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) @@ -190,8 +189,11 @@ def update_host(host_id: int, data: Dict[str, Any]) -> bool: 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() @@ -201,7 +203,7 @@ def update_host(host_id: int, data: Dict[str, Any]) -> bool: # ----------------------------- # DELETE HOST # ----------------------------- -def delete_host(host_id: int) -> bool: +def delete_host(host_id: int): # Validate input if host_id is None: @@ -210,8 +212,11 @@ def delete_host(host_id: int) -> bool: 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() diff --git a/backend/db/settings.py b/backend/db/settings.py index 44dc1d6..2904b55 100644 --- a/backend/db/settings.py +++ b/backend/db/settings.py @@ -38,6 +38,13 @@ TYPE_CASTERS = { # 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", diff --git a/backend/routes/about.py b/backend/routes/about.py deleted file mode 100644 index 95ffe31..0000000 --- a/backend/routes/about.py +++ /dev/null @@ -1,43 +0,0 @@ -# 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..."} diff --git a/backend/routes/aliases.py b/backend/routes/aliases.py index 7569aba..e1f2622 100644 --- a/backend/routes/aliases.py +++ b/backend/routes/aliases.py @@ -1,9 +1,8 @@ # 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 @@ -31,7 +30,7 @@ router = APIRouter() # --------------------------------------------------------- # Aliass page @router.get("/aliases") -def aliases_page(request: Request): +def aliases_page(): return FileResponse(settings.FRONTEND_PATH / "aliases.html") # Serve aliases.js @@ -46,14 +45,12 @@ def 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( @@ -73,31 +70,13 @@ def api_get_aliases(request: Request): 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()) @@ -115,34 +94,52 @@ def api_get_alias(request: Request, alias_id: int): }, ) + 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, @@ -157,9 +154,6 @@ def api_add_alias(request: Request, data: dict): }, ) - 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 @@ -184,26 +178,26 @@ def api_add_alias(request: Request, data: dict): 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, @@ -218,9 +212,6 @@ def api_update_alias(request: Request, data: dict, alias_id: int): }, ) - 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 @@ -245,26 +236,26 @@ def api_update_alias(request: Request, data: dict, alias_id: int): 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, @@ -279,9 +270,6 @@ def api_delete_alias(request: Request, alias_id: int): }, ) - 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 diff --git a/backend/routes/backup.py b/backend/routes/backup.py index 4bf2326..41edbde 100644 --- a/backend/routes/backup.py +++ b/backend/routes/backup.py @@ -13,8 +13,7 @@ import zipfile # 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 @@ -45,8 +44,10 @@ def to_partial_code(code_ok: str) -> str: 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, @@ -56,9 +57,24 @@ def build_operation_response( 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: @@ -106,10 +122,12 @@ async def api_backup_create(): 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, @@ -144,9 +162,6 @@ async def api_backup_list(): "backups": backups } - except HTTPException: - raise - except Exception as err: logger.exception("Error listing backups: %s", str(err)) raise HTTPException( @@ -168,10 +183,12 @@ async def api_backup_restore(payload: BackupRestoreRequest): 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, @@ -199,27 +216,18 @@ async def api_backup_delete(payload: BackupDeleteRequest): 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 @@ -238,17 +246,17 @@ async def api_backup_delete(payload: BackupDeleteRequest): 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, @@ -298,7 +306,6 @@ def upload_backup(file: UploadFile = File(...)): ) # validate ZIP - import zipfile try: with zipfile.ZipFile(file.file) as z: if z.testzip() is not None: diff --git a/backend/routes/devices.py b/backend/routes/devices.py index fae6442..f1a8322 100644 --- a/backend/routes/devices.py +++ b/backend/routes/devices.py @@ -2,10 +2,8 @@ # 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 @@ -30,7 +28,7 @@ router = APIRouter() # --------------------------------------------------------- # Devices page @router.get("/devices") -def devices_page(request: Request): +def devices_page(): return FileResponse(settings.FRONTEND_PATH / "devices.html") # Serve devices.js @@ -45,27 +43,31 @@ def 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()) diff --git a/backend/routes/dhcp.py b/backend/routes/dhcp.py index 55e8229..4a075ed 100644 --- a/backend/routes/dhcp.py +++ b/backend/routes/dhcp.py @@ -1,7 +1,7 @@ # 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 @@ -28,7 +28,7 @@ router = APIRouter() # --------------------------------------------------------- # Leases page @router.get("/leases") -def leases_page(request: Request): +def leases_page(): return FileResponse(settings.FRONTEND_PATH / "leases.html") # Serve leases.js @@ -43,7 +43,7 @@ def 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() @@ -121,7 +121,7 @@ async def api_dhcp_reload(request: Request): 404: {"description": "Leases not found"}, 500: {"description": "Internal server error"}, }) -def api_dhcp_leases(request: Request): +def api_dhcp_leases(): try: leases = get_leases() @@ -131,7 +131,7 @@ def api_dhcp_leases(request: Request): 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), }, @@ -159,7 +159,7 @@ def api_dhcp_leases(request: Request): 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() @@ -224,7 +224,7 @@ def api_get_lease(request: Request, lease_id: int): 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() @@ -248,7 +248,7 @@ def api_delete_lease(request: Request, lease_id: int): 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": { @@ -263,7 +263,7 @@ def api_delete_lease(request: Request, lease_id: int): 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": { diff --git a/backend/routes/dns.py b/backend/routes/dns.py index b39173c..404177f 100644 --- a/backend/routes/dns.py +++ b/backend/routes/dns.py @@ -1,7 +1,7 @@ # 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 @@ -32,7 +32,7 @@ router = APIRouter() 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() diff --git a/backend/routes/health.py b/backend/routes/health.py index c1dd440..e8a309e 100644 --- a/backend/routes/health.py +++ b/backend/routes/health.py @@ -24,7 +24,7 @@ router = APIRouter() def health(): start = time.time() - db_status = "ok" + db_status = "healthy" db_version = None db_tables = None db_size = None @@ -51,7 +51,7 @@ def health(): 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, diff --git a/backend/routes/hosts.py b/backend/routes/hosts.py index 26f383a..f85ae80 100644 --- a/backend/routes/hosts.py +++ b/backend/routes/hosts.py @@ -1,9 +1,8 @@ # 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 @@ -31,7 +30,7 @@ router = APIRouter() # --------------------------------------------------------- # Hosts page @router.get("/hosts") -def hosts_page(request: Request): +def hosts_page(): return FileResponse(settings.FRONTEND_PATH / "hosts.html") # Serve hosts.js @@ -46,15 +45,12 @@ def 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( @@ -62,7 +58,7 @@ def api_get_hosts(request: Request): detail={ "code": "HOSTS_GET_ERROR", "status": "failure", - "message": "Internal error getting host", + "message": "Internal error getting hosts", }, ) @@ -74,31 +70,13 @@ def api_get_hosts(request: Request): 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()) @@ -116,34 +94,52 @@ def api_get_host(request: Request, host_id: int): }, ) + 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, @@ -158,9 +154,6 @@ def api_add_host(request: Request, data: dict): }, ) - 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 @@ -185,26 +178,26 @@ def api_add_host(request: Request, data: dict): 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, @@ -219,9 +212,6 @@ def api_update_host(request: Request, data: dict, host_id: int): }, ) - 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 @@ -246,26 +236,26 @@ def api_update_host(request: Request, data: dict, host_id: int): 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, @@ -280,9 +270,6 @@ def api_delete_host(request: Request, host_id: int): }, ) - 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 diff --git a/backend/routes/localization.py b/backend/routes/localization.py new file mode 100644 index 0000000..17ce50f --- /dev/null +++ b/backend/routes/localization.py @@ -0,0 +1,87 @@ +# 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 diff --git a/backend/routes/settings.py b/backend/routes/settings.py index 2802698..c23e295 100644 --- a/backend/routes/settings.py +++ b/backend/routes/settings.py @@ -1,9 +1,8 @@ # 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 @@ -29,7 +28,7 @@ router = APIRouter() # --------------------------------------------------------- # Settings page @router.get("/settings") -def settings_page(request: Request): +def settings_page(): return FileResponse(settings.FRONTEND_PATH / "settings.html") # Serve settings.js @@ -44,15 +43,12 @@ def 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( @@ -72,31 +68,13 @@ def api_get_configs(request: Request): 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()) @@ -114,16 +92,32 @@ def api_get_setting(request: Request, config_key: str): }, ) + 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() @@ -165,7 +159,7 @@ def api_update_setting(request: Request, data: dict, config_key: str): 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": { @@ -198,12 +192,12 @@ def api_update_setting(request: Request, data: dict, config_key: str): # 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() @@ -213,9 +207,9 @@ def api_reset_config(request: Request, config_key: str): 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, @@ -245,9 +239,9 @@ def api_reset_config(request: Request, config_key: str): 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, @@ -264,9 +258,9 @@ def api_reset_config(request: Request, config_key: str): 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, diff --git a/backend/routes/system.py b/backend/routes/system.py index ad20b74..b30fe73 100644 --- a/backend/routes/system.py +++ b/backend/routes/system.py @@ -34,10 +34,18 @@ def about(): # --------------------------------------------------------- @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..." + } diff --git a/backend/settings/default.py b/backend/settings/default.py index 8e2fe3a..d1da838 100644 --- a/backend/settings/default.py +++ b/backend/settings/default.py @@ -16,6 +16,11 @@ DATA_PATH = "/data" DB_FILE = "database.db" DB_RESET = False +# --------------------------------------------------------- +# Language +# --------------------------------------------------------- +LANGUAGE = "en" + # --------------------------------------------------------- # Log # --------------------------------------------------------- diff --git a/backend/settings/localization.py b/backend/settings/localization.py new file mode 100644 index 0000000..2c56493 --- /dev/null +++ b/backend/settings/localization.py @@ -0,0 +1,41 @@ +# 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) diff --git a/backend/settings/settings.py b/backend/settings/settings.py index a5e05f0..54e63e5 100644 --- a/backend/settings/settings.py +++ b/backend/settings/settings.py @@ -79,6 +79,9 @@ class Settings(BaseModel): 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)) diff --git a/frontend/aliases.html b/frontend/aliases.html index 0c87f1f..911160e 100644 --- a/frontend/aliases.html +++ b/frontend/aliases.html @@ -20,7 +20,7 @@ - +
@@ -33,7 +33,7 @@ - Network Manager + Network Manager @@ -62,24 +62,26 @@
-
Network Manager
+
Network Manager
@@ -98,7 +100,7 @@

- Aliases List + Aliases List

@@ -111,9 +113,9 @@ + data-i18n-placeholder="common.search.placeholder" + data-i18n-aria-label="aliases.search">
@@ -136,11 +138,11 @@ @@ -149,21 +151,21 @@ @@ -182,11 +184,11 @@
  • @@ -194,11 +196,11 @@
  • @@ -206,11 +208,11 @@
  • @@ -223,9 +225,9 @@ + data-i18n-placeholder="common.search.placeholder" + data-i18n-aria-label="aliases.search"> @@ -236,23 +238,27 @@ - Alias + Alias + - Target + Target + - Description + Description + - Options + Options + - Actions + Actions @@ -263,10 +269,9 @@ -
    diff --git a/frontend/css/layout.css b/frontend/css/layout.css index d729c97..cb61317 100644 --- a/frontend/css/layout.css +++ b/frontend/css/layout.css @@ -728,7 +728,7 @@ td.actions { .table-responsive { margin: 10px; - max-height: calc(100dvh - 9rem); + max-height: calc(100dvh - 8.5rem); } .table { diff --git a/frontend/devices.html b/frontend/devices.html index 8c68658..bbe6273 100644 --- a/frontend/devices.html +++ b/frontend/devices.html @@ -20,7 +20,7 @@ - +
    @@ -33,7 +33,7 @@ - Network Manager + Network Manager @@ -62,24 +62,26 @@
    -
    Network Manager
    +
    Network Manager
    @@ -98,7 +100,7 @@

    - Devices List + Devices List

    @@ -111,9 +113,9 @@ + data-i18n-placeholder="common.search.placeholder" + data-i18n-aria-label="devices.search">
    @@ -136,11 +138,11 @@ @@ -149,21 +151,21 @@ @@ -182,11 +184,11 @@
  • @@ -194,11 +196,11 @@
  • @@ -206,11 +208,11 @@
  • @@ -223,9 +225,9 @@ + data-i18n-placeholder="common.search.placeholder" + data-i18n-aria-label="devices.search"> @@ -236,31 +238,37 @@ - IP Address + IP Address + - MAC Address + MAC Address + - Hostname + Hostname + - Description + Description + - State + State + - Active + Active + - Actions + Actions @@ -271,10 +279,9 @@ -
    diff --git a/frontend/hosts.html b/frontend/hosts.html index ddfd894..7e5696e 100644 --- a/frontend/hosts.html +++ b/frontend/hosts.html @@ -20,7 +20,7 @@ - +
    @@ -33,7 +33,7 @@ - Network Manager + Network Manager @@ -62,24 +62,26 @@
    -
    Network Manager
    +
    Network Manager
    @@ -98,7 +100,7 @@

    - Hosts List + Hosts List

    @@ -111,9 +113,9 @@ + data-i18n-placeholder="common.search.placeholder" + data-i18n-aria-label="hosts.search">
    @@ -136,11 +138,11 @@ @@ -149,21 +151,21 @@ @@ -182,11 +184,11 @@
  • @@ -194,11 +196,11 @@
  • @@ -206,11 +208,11 @@
  • @@ -223,9 +225,9 @@ + data-i18n-placeholder="common.search.placeholder" + data-i18n-aria-label="hosts.search"> @@ -236,27 +238,32 @@ - Hostname + Hostname + - IP Address + IP Address + - MAC Address + MAC Address + - Description + Description + - Options + Options + - Actions + Actions @@ -267,10 +274,9 @@ -
    diff --git a/frontend/index.html b/frontend/index.html index 276285f..a0ae91d 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -20,7 +20,7 @@ - +
    @@ -33,7 +33,7 @@ - Network Manager + Network Manager @@ -57,19 +57,21 @@
    -
    Network Manager
    +
    Network Manager
    - - + +
    @@ -88,7 +90,7 @@

    - Dashboard + Dashboard

    @@ -105,14 +107,14 @@
    -

    DNS (BIND)

    -

    Zone, record e configurazioni.

    +

    DNS (BIND)

    +

    Zones, records and configuration.

    - - DNS Configuration + + DNS Configuration -
    @@ -120,14 +122,14 @@
    -

    DHCP (Kea)

    -

    Pools, leases, reservations.

    +

    DHCP (Kea)

    +

    Pools, leases, reservations.

    - - DHCP Leases + + DHCP Leases -
    @@ -135,43 +137,43 @@
    -

    Hosts

    -

    Inventario IP/MAC.

    +

    Hosts

    +

    IP/MAC inventory.

    -

    Aliases

    -

    Inventario alias DNS.

    +

    Aliases

    +

    DNS alias inventory.

    -

    Devices

    -

    Device status (Host + DHCP).

    +

    Devices

    +

    Device status (Host + DHCP).

    -

    Certificati

    -

    Let’s Encrypt e rinnovi.

    +

    Certificates

    +

    Let's Encrypt certificates and renewals.

    -

    Backup & Restore

    -

    Esecuzione backup e gestione archivi.

    +

    Backup & Restore

    +

    Backup execution and archive management.

    - -
    @@ -179,22 +181,22 @@
    -

    Logs

    -

    Eventi e access log.

    +

    Logs

    +

    Events and access logs.

    -

    Impostazioni

    -

    Configurazione sistema e variabili.

    +

    Settings

    +

    System configuration and variables.

    -

    Health

    -

    Stato servizi e risorse.

    +

    Health

    +

    Service and resource status.

    @@ -204,7 +206,7 @@
    - + diff --git a/frontend/js/aliases.js b/frontend/js/aliases.js index f3eb2e1..ee1bcc5 100644 --- a/frontend/js/aliases.js +++ b/frontend/js/aliases.js @@ -1,7 +1,10 @@ -// 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 @@ -30,8 +33,8 @@ async function fetchAliases () { 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 @@ -64,7 +67,7 @@ function updateTable () { 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); @@ -77,15 +80,15 @@ function updateTable () { // 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); @@ -94,7 +97,7 @@ function updateTable () { // 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); @@ -103,7 +106,7 @@ function updateTable () { // 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); @@ -118,53 +121,56 @@ function updateTable () { // // 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); } @@ -177,11 +183,12 @@ function updateTable () { // 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"); @@ -195,11 +202,12 @@ function updateTable () { // 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)); { @@ -237,7 +245,7 @@ function updateTable () { document.getElementById('searchInput')?.value || document.getElementById('searchInputMobile')?.value; if (term?.trim()) { - handleSearch(term); + handleSearch(term, filterTable); } } @@ -267,8 +275,8 @@ async function editAlias(id) { 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); } } @@ -278,12 +286,12 @@ async function editAlias(id) { 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; } @@ -298,19 +306,27 @@ async function saveAlias(aliasData) { 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; @@ -366,8 +382,20 @@ async function handleAddAliasSubmit(e) { } } 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; @@ -383,17 +411,13 @@ async function handleDeleteAlias(e, el) { // 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 @@ -403,8 +427,8 @@ async function handleDeleteAlias(e, el) { 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; @@ -427,9 +451,9 @@ const actionHandlers = { 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 @@ -437,9 +461,9 @@ const actionHandlers = { await handleReload( el, serviceReloadDHCP, - "DHCP reload successfully", - "Error reloading DHCP", - "Reloading DHCP..." + t("dhcp.reload.ok"), + t("dhcp.reload.error"), + t("dhcp.reload.progress") ); }, }; @@ -456,21 +480,32 @@ document.addEventListener("DOMContentLoaded", async () => { // ----------------------------- 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(); @@ -503,7 +538,7 @@ function initSearch() { // live filter input.addEventListener("input", (e) => { - handleSearch(e.target.value); + handleSearch(e.target.value, filterTable); }); }); } @@ -532,7 +567,7 @@ function initModalLifecycle() { 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(); @@ -590,8 +625,8 @@ async function handleActionClick(e) { 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); } } diff --git a/frontend/js/api.js b/frontend/js/api.js index 4bd35b2..faf1c15 100644 --- a/frontend/js/api.js +++ b/frontend/js/api.js @@ -53,17 +53,17 @@ export async function apiRequest( // 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; } diff --git a/frontend/js/backendMessages.js b/frontend/js/backendMessages.js new file mode 100644 index 0000000..de1e873 --- /dev/null +++ b/frontend/js/backendMessages.js @@ -0,0 +1,80 @@ +// ------------------------------------------------------- +// 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) : ""; +} diff --git a/frontend/js/common.js b/frontend/js/common.js index f77e278..97d5d34 100644 --- a/frontend/js/common.js +++ b/frontend/js/common.js @@ -1,3 +1,8 @@ +// ------------------------------------------------------- +// IMPORT +// ------------------------------------------------------- +import { getBackendMessage } from "./backendMessages.js"; + // ----------------------------- // Configuration parameters // ----------------------------- @@ -8,23 +13,20 @@ const stringCollator = new Intl.Collator(undefined, { numeric: true, sensitivity // 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; } // ----------------------------- @@ -440,7 +442,6 @@ export async function handleReload( const originalHTML = button.innerHTML; - // spinner + testo button.innerHTML = ` ${workingText} @@ -448,16 +449,18 @@ export async function handleReload( 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) { diff --git a/frontend/js/devices.js b/frontend/js/devices.js index 9748598..c36c984 100644 --- a/frontend/js/devices.js +++ b/frontend/js/devices.js @@ -1,7 +1,10 @@ -// 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 @@ -30,8 +33,8 @@ async function fetchDevices () { 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 @@ -64,7 +67,7 @@ function updateTable () { 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); @@ -89,8 +92,8 @@ function updateTable () { } 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"); @@ -139,47 +142,47 @@ function updateTable () { 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); } @@ -188,20 +191,20 @@ function updateTable () { 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); } @@ -215,11 +218,12 @@ function updateTable () { // 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"); @@ -233,11 +237,12 @@ function updateTable () { // 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"); @@ -251,11 +256,12 @@ function updateTable () { // 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)); { @@ -322,11 +328,11 @@ async function editHost(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 { @@ -355,10 +361,9 @@ async function editHost(id) { } 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); } } @@ -368,22 +373,22 @@ async function editHost(id) { 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; } @@ -398,19 +403,27 @@ async function saveHost(hostData) { 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; @@ -468,8 +481,20 @@ async function handleAddHostSubmit(e) { } } 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; @@ -489,8 +514,7 @@ async function handleDeleteDevice(e, el) { 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; } @@ -504,25 +528,23 @@ async function handleDeleteDevice(e, el) { // 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 @@ -532,8 +554,8 @@ async function handleDeleteDevice(e, el) { 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; @@ -547,7 +569,11 @@ const actionHandlers = { 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 }, @@ -556,9 +582,9 @@ const actionHandlers = { 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 @@ -566,9 +592,9 @@ const actionHandlers = { await handleReload( el, serviceReloadDHCP, - "DHCP reload successfully", - "Error reloading DHCP", - "Reloading DHCP..." + t("dhcp.reload.ok"), + t("dhcp.reload.error"), + t("dhcp.reload.progress") ); }, }; @@ -585,21 +611,32 @@ document.addEventListener("DOMContentLoaded", async () => { // ----------------------------- 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(); @@ -661,7 +698,7 @@ function initModalLifecycle() { 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(); @@ -719,8 +756,8 @@ async function handleActionClick(e) { 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); } } diff --git a/frontend/js/hosts.js b/frontend/js/hosts.js index 788350a..5dda214 100644 --- a/frontend/js/hosts.js +++ b/frontend/js/hosts.js @@ -1,7 +1,10 @@ -// 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 @@ -30,8 +33,8 @@ async function fetchHosts () { 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 @@ -64,7 +67,7 @@ function updateTable () { 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); @@ -129,52 +132,55 @@ function updateTable () { // 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); } @@ -187,11 +193,12 @@ function updateTable () { // 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"); @@ -205,11 +212,12 @@ function updateTable () { // 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)); { @@ -278,10 +286,9 @@ async function editHost(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); } } @@ -291,22 +298,22 @@ async function editHost(id) { 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; } @@ -321,19 +328,27 @@ async function saveHost(hostData) { 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; @@ -391,8 +406,20 @@ async function handleAddHostSubmit(e) { } } 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; @@ -408,17 +435,13 @@ async function handleDeleteHost(e, el) { // 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 @@ -428,8 +451,8 @@ async function handleDeleteHost(e, el) { 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; @@ -452,9 +475,9 @@ const actionHandlers = { 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 @@ -462,9 +485,9 @@ const actionHandlers = { await handleReload( el, serviceReloadDHCP, - "DHCP reload successfully", - "Error reloading DHCP", - "Reloading DHCP..." + t("dhcp.reload.ok"), + t("dhcp.reload.error"), + t("dhcp.reload.progress") ); }, }; @@ -481,21 +504,32 @@ document.addEventListener("DOMContentLoaded", async () => { // ----------------------------- 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(); @@ -557,7 +591,7 @@ function initModalLifecycle() { 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(); @@ -615,8 +649,8 @@ async function handleActionClick(e) { 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); } } diff --git a/frontend/js/i18n.js b/frontend/js/i18n.js new file mode 100644 index 0000000..c117146 --- /dev/null +++ b/frontend/js/i18n.js @@ -0,0 +1,72 @@ +// ------------------------------------------------------- +// 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 + ); + }); +} diff --git a/frontend/js/index.js b/frontend/js/index.js index 9903780..ed619c7 100644 --- a/frontend/js/index.js +++ b/frontend/js/index.js @@ -3,6 +3,29 @@ // ------------------------------------------------------- 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 @@ -14,22 +37,25 @@ async function openBackupModal() { modal.style.display = 'flex'; const tbody = document.getElementById("backupList"); - if (tbody) { - tbody.innerHTML = ` - - - Loading backups... - - - `; - } + if (!tbody) return; + tbody.innerHTML = ` + + + ${t("backup.loading")} + + + `; // 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); } } @@ -44,19 +70,19 @@ function closeBackupModal() { 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"); } }); @@ -68,13 +94,14 @@ export async function serviceCheckAbout() { // ------------------------------------------------------- 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 = ` - No backups available + ${t("backup.empty")} `; @@ -123,17 +150,21 @@ function renderBackupList(data) { 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 = ``; + 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 = ``; + deleteBtn.setAttribute("aria-label", deleteText); deleteBtn.setAttribute("data-action", "deleteBackup"); deleteBtn.setAttribute("data-id", b.name); tdActions.appendChild(deleteBtn); @@ -198,13 +229,13 @@ function openHealthModal() { 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); @@ -214,7 +245,7 @@ function openHealthModal() { .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); }); } @@ -229,13 +260,19 @@ function setHealthBadge(status) { 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) { @@ -254,16 +291,16 @@ 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) { @@ -290,7 +327,6 @@ const actionHandlers = { // Create Backup startBackup: async (e, el) => { const btn = el; - const modal = document.getElementById('backupModal'); if (!btn) return; @@ -298,16 +334,24 @@ const actionHandlers = { 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; @@ -317,17 +361,21 @@ const actionHandlers = { 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; } @@ -335,18 +383,26 @@ const actionHandlers = { 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; @@ -360,39 +416,50 @@ const actionHandlers = { 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 @@ -407,20 +474,20 @@ const actionHandlers = { 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; } @@ -439,15 +506,12 @@ const actionHandlers = { 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; @@ -459,7 +523,11 @@ const actionHandlers = { 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 @@ -469,9 +537,9 @@ const actionHandlers = { 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 @@ -479,18 +547,18 @@ const actionHandlers = { 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 @@ -512,10 +580,13 @@ document.addEventListener("DOMContentLoaded", async () => { 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(); }); @@ -531,11 +602,12 @@ document.addEventListener('click', async (e) => { 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); } }); @@ -569,4 +641,16 @@ async function periodicTest() { 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(); diff --git a/frontend/js/leases.js b/frontend/js/leases.js index 3e139e6..8d3e5bc 100644 --- a/frontend/js/leases.js +++ b/frontend/js/leases.js @@ -1,7 +1,10 @@ -// 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 @@ -29,8 +32,8 @@ async function fetchLeases () { 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 @@ -42,7 +45,7 @@ async function fetchLeases () { } // ----------------------------- -// Update table with current hosts +// Update table with current leases // ----------------------------- function updateTable () { const loader = document.getElementById("loader"); @@ -63,7 +66,7 @@ function updateTable () { 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); @@ -155,41 +158,41 @@ function updateTable () { 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); } @@ -202,11 +205,12 @@ function updateTable () { // 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"); @@ -220,11 +224,12 @@ function updateTable () { // 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)); { @@ -292,8 +297,8 @@ async function addHost(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); } } @@ -303,39 +308,45 @@ async function addHost(id) { 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; @@ -383,7 +394,7 @@ async function handleAddHostSubmit(e) { const ok = await saveHost(data); if (ok !== false) { - // close modal and reload hosts + // close modal and reload leases closeAddHostModal(); await fetchLeases(); updateTable(); @@ -391,8 +402,8 @@ async function handleAddHostSubmit(e) { } } 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; @@ -408,17 +419,14 @@ async function handleDeleteLease(e, el) { // 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 @@ -428,8 +436,8 @@ async function handleDeleteLease(e, el) { 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; @@ -452,9 +460,9 @@ const actionHandlers = { 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 @@ -462,9 +470,9 @@ const actionHandlers = { await handleReload( el, serviceReloadDHCP, - "DHCP reload successfully", - "Error reloading DHCP", - "Reloading DHCP..." + t("dhcp.reload.ok"), + t("dhcp.reload.error"), + t("dhcp.reload.progress") ); }, }; @@ -481,21 +489,32 @@ document.addEventListener("DOMContentLoaded", async () => { // ----------------------------- 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(); @@ -556,7 +575,7 @@ function initModalLifecycle() { 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(); @@ -607,8 +626,8 @@ async function handleActionClick(e) { 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); } } diff --git a/frontend/js/login.js b/frontend/js/login.js index 31d0422..f3c043d 100644 --- a/frontend/js/login.js +++ b/frontend/js/login.js @@ -1,3 +1,8 @@ +// ------------------------------------------------------- +// IMPORT +// ------------------------------------------------------- +import { loadLanguage, translatePage, t } from "./i18n.js"; + // ----------------------------- // DOMContentLoaded: initialize everything // ----------------------------- diff --git a/frontend/js/logs.js b/frontend/js/logs.js index 92325a7..0cad8e1 100644 --- a/frontend/js/logs.js +++ b/frontend/js/logs.js @@ -1,7 +1,10 @@ -// 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 @@ -107,7 +110,8 @@ function clearFilters() { updateFilterButton(); requestAnimationFrame(() => { - logViewer.scrollTop = savedScrollPosition; + if (savedScrollPosition !== null) + logViewer.scrollTop = savedScrollPosition; }); } @@ -376,7 +380,7 @@ function startReconnectPolling(button, originalHtmlButton) { 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; @@ -390,13 +394,13 @@ function startReconnectPolling(button, originalHtmlButton) { 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 @@ -406,7 +410,7 @@ function startReconnectPolling(button, originalHtmlButton) { // 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; @@ -414,9 +418,9 @@ async function handleRestartApp(button) { 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 ); @@ -443,24 +447,22 @@ const actionHandlers = { //}, // 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 @@ -473,8 +475,8 @@ const actionHandlers = { // DOMContentLoaded: bootstrap app // ----------------------------- document.addEventListener("DOMContentLoaded", async () => { - initApp(); - loadLogs(); + await initApp(); + await loadLogs(); }); // ----------------------------- @@ -482,14 +484,25 @@ document.addEventListener("DOMContentLoaded", async () => { // ----------------------------- 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(); @@ -530,8 +543,8 @@ async function handleActionClick(e) { 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); } } diff --git a/frontend/js/services.js b/frontend/js/services.js index f8fefab..5ae247b 100644 --- a/frontend/js/services.js +++ b/frontend/js/services.js @@ -24,26 +24,22 @@ export async function serviceCheckHealth() { // 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; } // ----------------------------- @@ -68,13 +64,11 @@ export async function serviceGetDHCPLease(id) { // 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; } // ----------------------------- @@ -99,7 +93,7 @@ export async function serviceGetHost(id) { // Create a new host // ----------------------------- export async function serviceCreateHost(hostData) { - const data = await apiRequest( + return await apiRequest( "/api/hosts", { method: "POST", @@ -108,15 +102,13 @@ export async function serviceCreateHost(hostData) { }, "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", @@ -125,21 +117,17 @@ export async function serviceUpdateHost(id, hostData) { }, "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; } // ----------------------------- @@ -164,7 +152,7 @@ export async function serviceGetAlias(id) { // Create a new alias // ----------------------------- export async function serviceCreateAlias(aliasData) { - const data = await apiRequest( + return await apiRequest( "/api/aliases", { method: "POST", @@ -173,15 +161,13 @@ export async function serviceCreateAlias(aliasData) { }, "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", @@ -190,21 +176,17 @@ export async function serviceUpdateAlias(id, aliasData) { }, "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; } // ----------------------------- @@ -218,23 +200,12 @@ export async function serviceGetDevices() { // 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; } // ------------------------------------------------------- @@ -248,36 +219,22 @@ export async function serviceBackupList() { // 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; } // ------------------------------------------------------- @@ -344,7 +301,7 @@ export async function serviceGetConfig(key) { // Update a configuration parameter // ----------------------------- export async function serviceUpdateConfig(key, configData) { - const data = await apiRequest( + return await apiRequest( `/api/settings/${key}`, { method: "PUT", @@ -353,34 +310,28 @@ export async function serviceUpdateConfig(key, configData) { }, "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; } // ----------------------------- diff --git a/frontend/js/settings.js b/frontend/js/settings.js index 3f4a17d..0fa8c44 100644 --- a/frontend/js/settings.js +++ b/frontend/js/settings.js @@ -1,7 +1,10 @@ -// 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 @@ -71,8 +74,8 @@ async function fetchConfigs () { 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 @@ -164,7 +167,7 @@ function startReconnectPolling(button, originalHtmlButton) { 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; @@ -178,13 +181,13 @@ function startReconnectPolling(button, originalHtmlButton) { 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 @@ -194,7 +197,7 @@ function startReconnectPolling(button, originalHtmlButton) { // 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; @@ -202,9 +205,9 @@ async function handleRestartApp(button) { 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 ); @@ -226,11 +229,11 @@ function updateTable (filter = null) { 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 @@ -248,7 +251,7 @@ function updateTable (filter = null) { 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); @@ -358,8 +361,9 @@ function updateTable (filter = null) { 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"); @@ -373,11 +377,12 @@ function updateTable (filter = null) { // 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)); { @@ -534,8 +539,8 @@ async function editConfig(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); } } @@ -547,7 +552,7 @@ async function saveConfig(configData) { 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; @@ -557,13 +562,13 @@ async function saveConfig(configData) { 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; } @@ -572,17 +577,23 @@ async function saveConfig(configData) { // 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; @@ -629,12 +640,20 @@ async function handleEditConfigSubmit(e) { 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; @@ -650,21 +669,18 @@ async function handleResetConfig(e, el) { // 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 @@ -674,8 +690,8 @@ async function handleResetConfig(e, el) { 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; @@ -694,24 +710,22 @@ const actionHandlers = { // 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 @@ -732,21 +746,32 @@ document.addEventListener("DOMContentLoaded", async () => { // ----------------------------- 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(); @@ -806,14 +831,13 @@ function initModalLifecycle() { 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; } @@ -850,10 +874,10 @@ function initEvents() { 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)); } // ----------------------------- @@ -871,8 +895,8 @@ async function handleActionClick(e) { 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); } } diff --git a/frontend/leases.html b/frontend/leases.html index 065cd3f..db2b2af 100644 --- a/frontend/leases.html +++ b/frontend/leases.html @@ -20,7 +20,7 @@ - +
    @@ -33,7 +33,7 @@ - Network Manager + Network Manager @@ -62,24 +62,26 @@
    -
    Network Manager
    +
    Network Manager
    @@ -98,7 +100,7 @@

    - DHCP Leases + DHCP Leases

    @@ -111,9 +113,9 @@ + data-i18n-placeholder="common.search.placeholder" + data-i18n-aria-label="dhcp.leases.search">
    @@ -136,11 +138,11 @@ @@ -149,21 +151,21 @@ @@ -182,11 +184,11 @@
  • @@ -194,11 +196,11 @@
  • @@ -206,11 +208,11 @@
  • @@ -223,9 +225,9 @@ + data-i18n-placeholder="common.search.placeholder" + data-i18n-aria-label="dhcp.lease.search"> @@ -236,31 +238,37 @@ - IP Address + IP Address + - MAC + MAC Address + - Hostname + Hostname + - Start + Start + - End + End + - - State + + State + - Actions + Actions @@ -271,10 +279,9 @@ -
    diff --git a/frontend/locales/en.json b/frontend/locales/en.json new file mode 100644 index 0000000..87f3105 --- /dev/null +++ b/frontend/locales/en.json @@ -0,0 +1,307 @@ +{ + "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…", + "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 diff --git a/frontend/locales/it.json b/frontend/locales/it.json new file mode 100644 index 0000000..27d4766 --- /dev/null +++ b/frontend/locales/it.json @@ -0,0 +1,307 @@ +{ + "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 diff --git a/frontend/logs.html b/frontend/logs.html index 7f0bd3e..83c186b 100644 --- a/frontend/logs.html +++ b/frontend/logs.html @@ -20,7 +20,7 @@ - +
    @@ -33,7 +33,7 @@ - Network Manager + Network Manager @@ -62,24 +62,26 @@
    -
    Network Manager
    +
    Network Manager
    @@ -98,7 +100,7 @@

    - System Logs + System Logs

    @@ -115,7 +117,7 @@ class="btn btn-primary dropdown-toggle d-flex align-items-center gap-2" data-bs-toggle="dropdown"> - View + View @@ -153,7 +155,7 @@ class="btn btn-primary dropdown-toggle d-flex align-items-center gap-2 px-3" data-bs-toggle="dropdown"> - App + App @@ -282,10 +285,9 @@ -
    diff --git a/frontend/modals.html b/frontend/modals.html index b468c61..6730470 100644 --- a/frontend/modals.html +++ b/frontend/modals.html @@ -1,4 +1,4 @@ - +
    - +