fix ux
This commit is contained in:
@@ -79,7 +79,7 @@ class BucketPoint:
|
|||||||
label: str
|
label: str
|
||||||
start: datetime
|
start: datetime
|
||||||
end: datetime
|
end: datetime
|
||||||
value: float
|
value: float | None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from datetime import datetime, timezone
|
|||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
|
|
||||||
from flask import Blueprint, jsonify, request
|
from flask import Blueprint, jsonify, request, session
|
||||||
|
|
||||||
from app.core_settings import get_settings
|
from app.core_settings import get_settings
|
||||||
from app.services.capabilities import build_capabilities
|
from app.services.capabilities import build_capabilities
|
||||||
@@ -19,6 +19,27 @@ dashboard_blueprint = Blueprint("dashboard", __name__)
|
|||||||
_APP_STARTED_AT = datetime.now(timezone.utc)
|
_APP_STARTED_AT = datetime.now(timezone.utc)
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_kiosk_mode(requested_mode: str, require_write_access: bool = False) -> tuple[str, str]:
|
||||||
|
normalized_mode = (requested_mode or "private").strip().lower()
|
||||||
|
auth_service = get_auth_service()
|
||||||
|
|
||||||
|
if normalized_mode == "public":
|
||||||
|
if require_write_access and auth_service.enabled and session.get("auth_role") != "admin":
|
||||||
|
raise PermissionError("Brak uprawnien do edycji publicznego kiosku")
|
||||||
|
return "public", "public"
|
||||||
|
|
||||||
|
if normalized_mode != "private":
|
||||||
|
raise ValueError("Mode musi byc jednym z: public, private")
|
||||||
|
|
||||||
|
if (not auth_service.enabled) or session.get("auth_role") == "admin":
|
||||||
|
return "private", "private"
|
||||||
|
|
||||||
|
username = session.get("auth_user")
|
||||||
|
if not username:
|
||||||
|
raise PermissionError("Authentication required")
|
||||||
|
return f"user:{username}", "private"
|
||||||
|
|
||||||
|
|
||||||
@dashboard_blueprint.get("/dashboard/config")
|
@dashboard_blueprint.get("/dashboard/config")
|
||||||
def dashboard_config():
|
def dashboard_config():
|
||||||
settings = get_settings()
|
settings = get_settings()
|
||||||
@@ -65,8 +86,12 @@ def dashboard_config():
|
|||||||
def dashboard_kiosk_settings():
|
def dashboard_kiosk_settings():
|
||||||
requested_mode = request.args.get("mode") or ("public" if request.args.get("publicKiosk") == "1" else "private")
|
requested_mode = request.args.get("mode") or ("public" if request.args.get("publicKiosk") == "1" else "private")
|
||||||
try:
|
try:
|
||||||
payload = get_kiosk_settings_service().get(requested_mode)
|
storage_mode, response_mode = _resolve_kiosk_mode(requested_mode)
|
||||||
|
payload = get_kiosk_settings_service().get(storage_mode)
|
||||||
|
payload["mode"] = response_mode
|
||||||
return jsonify(to_plain(payload))
|
return jsonify(to_plain(payload))
|
||||||
|
except PermissionError as exc:
|
||||||
|
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
|
||||||
|
|
||||||
@@ -74,11 +99,11 @@ def dashboard_kiosk_settings():
|
|||||||
@dashboard_blueprint.put("/dashboard/kiosk-settings")
|
@dashboard_blueprint.put("/dashboard/kiosk-settings")
|
||||||
def update_dashboard_kiosk_settings():
|
def update_dashboard_kiosk_settings():
|
||||||
payload = request.get_json(silent=True) or {}
|
payload = request.get_json(silent=True) or {}
|
||||||
mode = payload.get("mode", "private")
|
requested_mode = payload.get("mode", "private")
|
||||||
auth_service = get_auth_service()
|
|
||||||
try:
|
try:
|
||||||
auth_service.require_admin()
|
storage_mode, response_mode = _resolve_kiosk_mode(requested_mode, require_write_access=True)
|
||||||
updated = get_kiosk_settings_service().update_from_session(mode, payload)
|
updated = get_kiosk_settings_service().update_from_session(storage_mode, payload)
|
||||||
|
updated["mode"] = response_mode
|
||||||
return jsonify(to_plain(updated))
|
return jsonify(to_plain(updated))
|
||||||
except PermissionError as exc:
|
except PermissionError as exc:
|
||||||
return jsonify({"detail": str(exc)}), 403
|
return jsonify({"detail": str(exc)}), 403
|
||||||
@@ -88,6 +113,12 @@ def update_dashboard_kiosk_settings():
|
|||||||
|
|
||||||
@dashboard_blueprint.get("/dashboard/diagnostics")
|
@dashboard_blueprint.get("/dashboard/diagnostics")
|
||||||
def dashboard_diagnostics():
|
def dashboard_diagnostics():
|
||||||
|
auth_service = get_auth_service()
|
||||||
|
try:
|
||||||
|
auth_service.require_admin()
|
||||||
|
except PermissionError as exc:
|
||||||
|
return jsonify({"detail": str(exc)}), 403
|
||||||
|
|
||||||
settings = get_settings()
|
settings = get_settings()
|
||||||
influx_diagnostics = InfluxHTTPService(settings).diagnose()
|
influx_diagnostics = InfluxHTTPService(settings).diagnose()
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
|
|||||||
@@ -4,20 +4,30 @@ from datetime import date
|
|||||||
|
|
||||||
from flask import Blueprint, jsonify, request
|
from flask import Blueprint, jsonify, request
|
||||||
|
|
||||||
|
from app.services.auth import get_auth_service
|
||||||
from app.services.historical_sync import get_historical_sync_service
|
from app.services.historical_sync import get_historical_sync_service
|
||||||
from app.utils.serialization import to_plain
|
from app.utils.serialization import to_plain
|
||||||
|
|
||||||
historical_blueprint = Blueprint("historical", __name__)
|
historical_blueprint = Blueprint("historical", __name__)
|
||||||
service = get_historical_sync_service()
|
service = get_historical_sync_service()
|
||||||
|
auth_service = get_auth_service()
|
||||||
|
|
||||||
|
|
||||||
@historical_blueprint.get("/historical/status")
|
@historical_blueprint.get("/historical/status")
|
||||||
def historical_status():
|
def historical_status():
|
||||||
|
try:
|
||||||
|
auth_service.require_admin()
|
||||||
|
except PermissionError as exc:
|
||||||
|
return jsonify({"detail": str(exc)}), 403
|
||||||
return jsonify(to_plain(service.status()))
|
return jsonify(to_plain(service.status()))
|
||||||
|
|
||||||
|
|
||||||
@historical_blueprint.post("/historical/start")
|
@historical_blueprint.post("/historical/start")
|
||||||
def historical_start():
|
def historical_start():
|
||||||
|
try:
|
||||||
|
auth_service.require_admin()
|
||||||
|
except PermissionError as exc:
|
||||||
|
return jsonify({"detail": str(exc)}), 403
|
||||||
payload = request.get_json(silent=True) or {}
|
payload = request.get_json(silent=True) or {}
|
||||||
try:
|
try:
|
||||||
status = service.start(
|
status = service.start(
|
||||||
@@ -35,6 +45,10 @@ def historical_start():
|
|||||||
|
|
||||||
@historical_blueprint.post("/historical/sync-now")
|
@historical_blueprint.post("/historical/sync-now")
|
||||||
def historical_sync_now():
|
def historical_sync_now():
|
||||||
|
try:
|
||||||
|
auth_service.require_admin()
|
||||||
|
except PermissionError as exc:
|
||||||
|
return jsonify({"detail": str(exc)}), 403
|
||||||
try:
|
try:
|
||||||
status = service.start(auto=True)
|
status = service.start(auto=True)
|
||||||
return jsonify(to_plain(status))
|
return jsonify(to_plain(status))
|
||||||
@@ -44,6 +58,10 @@ def historical_sync_now():
|
|||||||
|
|
||||||
@historical_blueprint.post("/historical/cancel")
|
@historical_blueprint.post("/historical/cancel")
|
||||||
def historical_cancel():
|
def historical_cancel():
|
||||||
|
try:
|
||||||
|
auth_service.require_admin()
|
||||||
|
except PermissionError as exc:
|
||||||
|
return jsonify({"detail": str(exc)}), 403
|
||||||
return jsonify(to_plain(service.cancel()))
|
return jsonify(to_plain(service.cancel()))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class AnalyticsService:
|
|||||||
window = resolve_window(range_key=range_key, start=start, end=end)
|
window = resolve_window(range_key=range_key, start=start, end=end)
|
||||||
current_days = self.energy.daily_records_for_window(window.start, window.end, persist_missing=True)
|
current_days = self.energy.daily_records_for_window(window.start, window.end, persist_missing=True)
|
||||||
current = self.energy.bucketize_daily(current_days, bucket)
|
current = self.energy.bucketize_daily(current_days, bucket)
|
||||||
total = round(sum(item.value for item in current), 2)
|
total = round(sum((item.value or 0.0) for item in current), 2)
|
||||||
|
|
||||||
comparison = []
|
comparison = []
|
||||||
comparison_total = None
|
comparison_total = None
|
||||||
@@ -52,7 +52,7 @@ class AnalyticsService:
|
|||||||
compare_window = resolve_window(start=compare_start, end=compare_end)
|
compare_window = resolve_window(start=compare_start, end=compare_end)
|
||||||
comparison_days = self.energy.daily_records_for_window(compare_window.start, compare_window.end, persist_missing=True)
|
comparison_days = self.energy.daily_records_for_window(compare_window.start, compare_window.end, persist_missing=True)
|
||||||
comparison_series = self.energy.bucketize_daily(comparison_days, bucket)
|
comparison_series = self.energy.bucketize_daily(comparison_days, bucket)
|
||||||
comparison_total_value = round(sum(point.value for point in comparison_series), 2)
|
comparison_total_value = round(sum((point.value or 0.0) for point in comparison_series), 2)
|
||||||
comparisons.append({
|
comparisons.append({
|
||||||
"key": item.get("key") or f"custom_{index + 1}",
|
"key": item.get("key") or f"custom_{index + 1}",
|
||||||
"label": item.get("label") or f"Custom {index + 1}",
|
"label": item.get("label") or f"Custom {index + 1}",
|
||||||
@@ -70,7 +70,7 @@ class AnalyticsService:
|
|||||||
compare_window = shift_window(window, compare_mode)
|
compare_window = shift_window(window, compare_mode)
|
||||||
comparison_days = self.energy.daily_records_for_window(compare_window.start, compare_window.end, persist_missing=True)
|
comparison_days = self.energy.daily_records_for_window(compare_window.start, compare_window.end, persist_missing=True)
|
||||||
comparison = self.energy.bucketize_daily(comparison_days, bucket)
|
comparison = self.energy.bucketize_daily(comparison_days, bucket)
|
||||||
comparison_total = round(sum(item.value for item in comparison), 2)
|
comparison_total = round(sum((item.value or 0.0) for item in comparison), 2)
|
||||||
comparison_delta_pct = compare_delta_pct(total, comparison_total)
|
comparison_delta_pct = compare_delta_pct(total, comparison_total)
|
||||||
comparisons.append({
|
comparisons.append({
|
||||||
"key": compare_mode,
|
"key": compare_mode,
|
||||||
@@ -82,8 +82,9 @@ class AnalyticsService:
|
|||||||
"points": comparison,
|
"points": comparison,
|
||||||
})
|
})
|
||||||
|
|
||||||
average_bucket = round(total / len(current), 2) if current else 0.0
|
populated_current = [item for item in current if item.value is not None]
|
||||||
best_bucket = max(current, key=lambda item: item.value, default=None)
|
average_bucket = round(total / len(populated_current), 2) if populated_current else 0.0
|
||||||
|
best_bucket = max(populated_current, key=lambda item: item.value or 0.0, default=None)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"unit": "kWh",
|
"unit": "kWh",
|
||||||
@@ -121,7 +122,7 @@ class AnalyticsService:
|
|||||||
) -> dict:
|
) -> dict:
|
||||||
payload = self.production(range_key=range_key, bucket=bucket, compare_mode="none", start=start, end=end)
|
payload = self.production(range_key=range_key, bucket=bucket, compare_mode="none", start=start, end=end)
|
||||||
current = payload["current"]
|
current = payload["current"]
|
||||||
total = round(sum(item.value for item in current), 2)
|
total = round(sum((item.value or 0.0) for item in current), 2)
|
||||||
denominator = total or 1.0
|
denominator = total or 1.0
|
||||||
return {
|
return {
|
||||||
"unit": payload["unit"],
|
"unit": payload["unit"],
|
||||||
@@ -134,7 +135,7 @@ class AnalyticsService:
|
|||||||
"share": round((item.value / denominator) * 100.0, 2),
|
"share": round((item.value / denominator) * 100.0, 2),
|
||||||
}
|
}
|
||||||
for item in current
|
for item in current
|
||||||
if item.value > 0
|
if item.value is not None and item.value > 0
|
||||||
],
|
],
|
||||||
"meta": payload["meta"],
|
"meta": payload["meta"],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ class EnergyService:
|
|||||||
return rows
|
return rows
|
||||||
|
|
||||||
def bucketize_daily(self, records: list[DailyEnergyRecord], bucket: str) -> list[BucketPoint]:
|
def bucketize_daily(self, records: list[DailyEnergyRecord], bucket: str) -> list[BucketPoint]:
|
||||||
grouped: dict[str, dict] = defaultdict(lambda: {"value": 0.0, "start": None, "end": None, "label": ""})
|
grouped: dict[str, dict] = defaultdict(lambda: {"value": 0.0, "start": None, "end": None, "label": "", "has_data": False})
|
||||||
|
|
||||||
for record in records:
|
for record in records:
|
||||||
start = datetime.combine(record.day, time.min, tzinfo=self.tz)
|
start = datetime.combine(record.day, time.min, tzinfo=self.tz)
|
||||||
@@ -137,7 +137,9 @@ class EnergyService:
|
|||||||
|
|
||||||
current = grouped[key]
|
current = grouped[key]
|
||||||
current["label"] = label
|
current["label"] = label
|
||||||
current["value"] += record.energy_kwh
|
if record.samples_count > 0:
|
||||||
|
current["value"] += record.energy_kwh
|
||||||
|
current["has_data"] = True
|
||||||
current["start"] = bucket_start if current["start"] is None else min(current["start"], bucket_start)
|
current["start"] = bucket_start if current["start"] is None else min(current["start"], bucket_start)
|
||||||
current["end"] = bucket_end if current["end"] is None else max(current["end"], bucket_end)
|
current["end"] = bucket_end if current["end"] is None else max(current["end"], bucket_end)
|
||||||
|
|
||||||
@@ -149,7 +151,7 @@ class EnergyService:
|
|||||||
label=item["label"],
|
label=item["label"],
|
||||||
start=item["start"],
|
start=item["start"],
|
||||||
end=item["end"],
|
end=item["end"],
|
||||||
value=round(item["value"], 2),
|
value=round(item["value"], 2) if item["has_data"] else None,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return rows
|
return rows
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from app.storage.kiosk_settings import SQLiteKioskSettingsRepository
|
|||||||
|
|
||||||
|
|
||||||
VALID_MODES = {"public", "private"}
|
VALID_MODES = {"public", "private"}
|
||||||
|
USER_MODE_PREFIX = "user:"
|
||||||
DEFAULT_WIDGETS = ["hero", "history", "strings", "status", "production", "comparison", "importStatus"]
|
DEFAULT_WIDGETS = ["hero", "history", "strings", "status", "production", "comparison", "importStatus"]
|
||||||
VALID_WIDGETS = {"hero", "quickMetrics", "history", "status", "strings", "production", "comparison", "distribution", "importStatus"}
|
VALID_WIDGETS = {"hero", "quickMetrics", "history", "status", "strings", "production", "comparison", "distribution", "importStatus"}
|
||||||
VALID_REALTIME_RANGES = {"today", "yesterday", "6h", "12h", "24h", "48h", "7d"}
|
VALID_REALTIME_RANGES = {"today", "yesterday", "6h", "12h", "24h", "48h", "7d"}
|
||||||
@@ -66,9 +67,11 @@ class KioskSettingsService:
|
|||||||
|
|
||||||
def _normalize_mode(self, mode: str) -> str:
|
def _normalize_mode(self, mode: str) -> str:
|
||||||
normalized = (mode or "").strip().lower()
|
normalized = (mode or "").strip().lower()
|
||||||
if normalized not in VALID_MODES:
|
if normalized in VALID_MODES:
|
||||||
raise ValueError("Mode musi byc jednym z: public, private")
|
return normalized
|
||||||
return normalized
|
if normalized.startswith(USER_MODE_PREFIX) and len(normalized) > len(USER_MODE_PREFIX):
|
||||||
|
return normalized
|
||||||
|
raise ValueError("Mode musi byc jednym z: public, private")
|
||||||
|
|
||||||
def _normalize_widgets(self, widgets: Any) -> list[str]:
|
def _normalize_widgets(self, widgets: Any) -> list[str]:
|
||||||
if not isinstance(widgets, list):
|
if not isinstance(widgets, list):
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -209,3 +209,56 @@ body {
|
|||||||
.text-break {
|
.text-break {
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.pv-subnav-shell {
|
||||||
|
min-height: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pv-menu-tab {
|
||||||
|
display: inline-flex !important;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.55rem;
|
||||||
|
border: 1px solid transparent !important;
|
||||||
|
border-radius: 999px !important;
|
||||||
|
padding: 0.6rem 0.95rem !important;
|
||||||
|
background: rgba(127, 127, 127, 0.08) !important;
|
||||||
|
color: inherit !important;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pv-menu-tab:hover,
|
||||||
|
.pv-menu-tab:focus {
|
||||||
|
background: rgba(127, 127, 127, 0.14) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pv-menu-tab.active {
|
||||||
|
background: rgba(32, 107, 196, 0.14) !important;
|
||||||
|
border-color: rgba(32, 107, 196, 0.22) !important;
|
||||||
|
color: var(--tblr-primary) !important;
|
||||||
|
box-shadow: 0 0 0 1px rgba(32, 107, 196, 0.06) inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pv-menu-tab-icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 1.1rem;
|
||||||
|
min-width: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pv-menu-meta {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.45rem;
|
||||||
|
color: var(--tblr-secondary);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 992px) {
|
||||||
|
.pv-menu-meta {
|
||||||
|
width: 100%;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export interface BucketPoint {
|
|||||||
label: string;
|
label: string;
|
||||||
start: string;
|
start: string;
|
||||||
end: string;
|
end: string;
|
||||||
value: number;
|
value: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AnalyticsSummary {
|
export interface AnalyticsSummary {
|
||||||
|
|||||||
Reference in New Issue
Block a user