ux
This commit is contained in:
@@ -1,5 +1,9 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
import os
|
||||||
|
import platform
|
||||||
|
|
||||||
from flask import Blueprint, jsonify, request
|
from flask import Blueprint, jsonify, request
|
||||||
|
|
||||||
from app.core_settings import get_settings
|
from app.core_settings import get_settings
|
||||||
@@ -7,10 +11,12 @@ from app.services.capabilities import build_capabilities
|
|||||||
from app.services.catalog import get_catalog
|
from app.services.catalog import get_catalog
|
||||||
from app.services.kiosk_settings import get_kiosk_settings_service
|
from app.services.kiosk_settings import get_kiosk_settings_service
|
||||||
from app.services.auth import get_auth_service
|
from app.services.auth import get_auth_service
|
||||||
|
from app.services.influx_http import InfluxHTTPService
|
||||||
from app.utils.serialization import to_plain
|
from app.utils.serialization import to_plain
|
||||||
|
|
||||||
|
|
||||||
dashboard_blueprint = Blueprint("dashboard", __name__)
|
dashboard_blueprint = Blueprint("dashboard", __name__)
|
||||||
|
_APP_STARTED_AT = datetime.now(timezone.utc)
|
||||||
|
|
||||||
|
|
||||||
@dashboard_blueprint.get("/dashboard/config")
|
@dashboard_blueprint.get("/dashboard/config")
|
||||||
@@ -78,3 +84,38 @@ def update_dashboard_kiosk_settings():
|
|||||||
return jsonify({"detail": str(exc)}), 403
|
return jsonify({"detail": str(exc)}), 403
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
return jsonify({"detail": str(exc)}), 400
|
return jsonify({"detail": str(exc)}), 400
|
||||||
|
|
||||||
|
|
||||||
|
@dashboard_blueprint.get("/dashboard/diagnostics")
|
||||||
|
def dashboard_diagnostics():
|
||||||
|
settings = get_settings()
|
||||||
|
influx_diagnostics = InfluxHTTPService(settings).diagnose()
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
payload = {
|
||||||
|
"app": {
|
||||||
|
"name": settings.app_name,
|
||||||
|
"version": settings.version,
|
||||||
|
"debug": settings.debug,
|
||||||
|
"timezone": settings.timezone,
|
||||||
|
"site_name": settings.site_name,
|
||||||
|
"installed_power_kwp": settings.installed_power_kwp,
|
||||||
|
"auth_enabled": settings.auth["enabled"],
|
||||||
|
"started_at": _APP_STARTED_AT.isoformat(),
|
||||||
|
"uptime_seconds": max(int((now - _APP_STARTED_AT).total_seconds()), 0),
|
||||||
|
"python_version": platform.python_version(),
|
||||||
|
"pid": os.getpid(),
|
||||||
|
},
|
||||||
|
"api": {
|
||||||
|
"status": "ok",
|
||||||
|
"prefix": settings.api_prefix,
|
||||||
|
"cors_origins_count": len(settings.cors_origins),
|
||||||
|
},
|
||||||
|
"influx": influx_diagnostics,
|
||||||
|
"storage": {
|
||||||
|
"sqlite_path": settings.storage["sqlite_path"],
|
||||||
|
"historical_import_enabled": settings.history["enabled"],
|
||||||
|
"auto_sync_enabled": settings.history["auto_sync_enabled"],
|
||||||
|
"default_chunk_days": settings.history["default_chunk_days"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return jsonify(to_plain(payload))
|
||||||
|
|||||||
@@ -165,6 +165,34 @@ class InfluxHTTPService:
|
|||||||
logger.warning("Influx last_before error for %s: %s", metric.id, exc)
|
logger.warning("Influx last_before error for %s: %s", metric.id, exc)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def diagnose(self) -> dict:
|
||||||
|
config = self.settings.influx
|
||||||
|
payload = {
|
||||||
|
"status": "connected",
|
||||||
|
"reachable": True,
|
||||||
|
"database_exists": False,
|
||||||
|
"url": self.base_url,
|
||||||
|
"database": config["database"],
|
||||||
|
"username_masked": _mask_secret(config.get("username") or ""),
|
||||||
|
"verify_ssl": bool(config.get("verify_ssl", False)),
|
||||||
|
"timeout_seconds": int(config.get("timeout_seconds", 15)),
|
||||||
|
"error": None,
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
series = self._execute("SHOW DATABASES")
|
||||||
|
databases: set[str] = set()
|
||||||
|
for item in series:
|
||||||
|
for row in self._rows_from_series(item):
|
||||||
|
value = row.get("name")
|
||||||
|
if isinstance(value, str):
|
||||||
|
databases.add(value)
|
||||||
|
payload["database_exists"] = config["database"] in databases
|
||||||
|
except Exception as exc:
|
||||||
|
payload["status"] = "error"
|
||||||
|
payload["reachable"] = False
|
||||||
|
payload["error"] = str(exc)
|
||||||
|
return payload
|
||||||
|
|
||||||
def _single_value(self, query: str) -> SeriesPoint | None:
|
def _single_value(self, query: str) -> SeriesPoint | None:
|
||||||
try:
|
try:
|
||||||
series = self._execute(query)
|
series = self._execute(query)
|
||||||
@@ -239,3 +267,11 @@ def _parse_time(value: str | None) -> datetime | None:
|
|||||||
return datetime.fromisoformat(value.replace("Z", "+00:00"))
|
return datetime.fromisoformat(value.replace("Z", "+00:00"))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _mask_secret(value: str) -> str:
|
||||||
|
if not value:
|
||||||
|
return ""
|
||||||
|
if len(value) <= 2:
|
||||||
|
return "*" * len(value)
|
||||||
|
return value[:1] + ("*" * max(len(value) - 2, 1)) + value[-1:]
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -3,6 +3,7 @@ import type {
|
|||||||
AuthStatus,
|
AuthStatus,
|
||||||
AuthUsersPayload,
|
AuthUsersPayload,
|
||||||
DashboardConfig,
|
DashboardConfig,
|
||||||
|
DiagnosticsPayload,
|
||||||
KioskSettingsPayload,
|
KioskSettingsPayload,
|
||||||
DistributionPayload,
|
DistributionPayload,
|
||||||
HistoryPayload,
|
HistoryPayload,
|
||||||
@@ -127,6 +128,47 @@ export const api = {
|
|||||||
DEMO_MODE
|
DEMO_MODE
|
||||||
? demoResponse(() => ({ ...demoHistoricalStatus, running: false, state: "cancelled", message: "Tryb demo: anulowano" }))
|
? demoResponse(() => ({ ...demoHistoricalStatus, running: false, state: "cancelled", message: "Tryb demo: anulowano" }))
|
||||||
: request<HistoricalStatus>("/historical/cancel", { method: "POST", body: JSON.stringify({}) }),
|
: request<HistoricalStatus>("/historical/cancel", { method: "POST", body: JSON.stringify({}) }),
|
||||||
|
getDiagnostics: (): Promise<DiagnosticsPayload> => (
|
||||||
|
DEMO_MODE
|
||||||
|
? demoResponse<DiagnosticsPayload>(() => ({
|
||||||
|
app: {
|
||||||
|
name: demoConfig.app.name,
|
||||||
|
version: demoConfig.app.version,
|
||||||
|
debug: true,
|
||||||
|
timezone: demoConfig.app.timezone,
|
||||||
|
site_name: demoConfig.app.site_name,
|
||||||
|
installed_power_kwp: demoConfig.app.installed_power_kwp,
|
||||||
|
auth_enabled: true,
|
||||||
|
started_at: new Date(Date.now() - 1000 * 60 * 42).toISOString(),
|
||||||
|
uptime_seconds: 1000 * 60 * 42,
|
||||||
|
python_version: "demo",
|
||||||
|
pid: 1,
|
||||||
|
},
|
||||||
|
api: {
|
||||||
|
status: "ok",
|
||||||
|
prefix: "/api/v1",
|
||||||
|
cors_origins_count: 2,
|
||||||
|
},
|
||||||
|
influx: {
|
||||||
|
status: "connected",
|
||||||
|
reachable: true,
|
||||||
|
database_exists: true,
|
||||||
|
url: "http://127.0.0.1:8086",
|
||||||
|
database: "ha",
|
||||||
|
username_masked: "demo",
|
||||||
|
verify_ssl: false,
|
||||||
|
timeout_seconds: 15,
|
||||||
|
error: null,
|
||||||
|
},
|
||||||
|
storage: {
|
||||||
|
sqlite_path: "/data/pv_insight.sqlite3",
|
||||||
|
historical_import_enabled: true,
|
||||||
|
auto_sync_enabled: true,
|
||||||
|
default_chunk_days: 7,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
: request<DiagnosticsPayload>("/dashboard/diagnostics")
|
||||||
|
),
|
||||||
getUsers: () => (DEMO_MODE ? demoResponse(() => ({ items: [] })) : request<AuthUsersPayload>("/auth/users")),
|
getUsers: () => (DEMO_MODE ? demoResponse(() => ({ items: [] })) : request<AuthUsersPayload>("/auth/users")),
|
||||||
createUser: (payload: { username: string; password: string; role: string; display_name?: string }) =>
|
createUser: (payload: { username: string; password: string; role: string; display_name?: string }) =>
|
||||||
DEMO_MODE ? demoResponse(() => payload) : request("/auth/users", { method: "POST", body: JSON.stringify(payload) }),
|
DEMO_MODE ? demoResponse(() => payload) : request("/auth/users", { method: "POST", body: JSON.stringify(payload) }),
|
||||||
|
|||||||
@@ -200,3 +200,12 @@ body {
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.pv-card .btn.text-start {
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-break {
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|||||||
@@ -273,3 +273,42 @@ export interface KioskSettingsPayload {
|
|||||||
updated_at?: string | null;
|
updated_at?: string | null;
|
||||||
updated_by?: string | null;
|
updated_by?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface DiagnosticsPayload {
|
||||||
|
app: {
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
debug: boolean;
|
||||||
|
timezone: string;
|
||||||
|
site_name: string;
|
||||||
|
installed_power_kwp: number;
|
||||||
|
auth_enabled: boolean;
|
||||||
|
started_at: string;
|
||||||
|
uptime_seconds: number;
|
||||||
|
python_version: string;
|
||||||
|
pid: number;
|
||||||
|
};
|
||||||
|
api: {
|
||||||
|
status: string;
|
||||||
|
prefix: string;
|
||||||
|
cors_origins_count: number;
|
||||||
|
};
|
||||||
|
influx: {
|
||||||
|
status: "connected" | "error";
|
||||||
|
reachable: boolean;
|
||||||
|
database_exists: boolean;
|
||||||
|
url: string;
|
||||||
|
database: string;
|
||||||
|
username_masked: string;
|
||||||
|
verify_ssl: boolean;
|
||||||
|
timeout_seconds: number;
|
||||||
|
error?: string | null;
|
||||||
|
};
|
||||||
|
storage: {
|
||||||
|
sqlite_path: string;
|
||||||
|
historical_import_enabled: boolean;
|
||||||
|
auto_sync_enabled: boolean;
|
||||||
|
default_chunk_days: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user