uxowe i funkcjonalne
This commit is contained in:
@@ -31,7 +31,7 @@ def _resolve_kiosk_mode(requested_mode: str, require_write_access: bool = False)
|
|||||||
if normalized_mode != "private":
|
if normalized_mode != "private":
|
||||||
raise ValueError("Mode musi byc jednym z: public, private")
|
raise ValueError("Mode musi byc jednym z: public, private")
|
||||||
|
|
||||||
if (not auth_service.enabled) or session.get("auth_role") == "admin":
|
if not auth_service.enabled:
|
||||||
return "private", "private"
|
return "private", "private"
|
||||||
|
|
||||||
username = session.get("auth_user")
|
username = session.get("auth_user")
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ from app.storage.kiosk_settings import SQLiteKioskSettingsRepository
|
|||||||
VALID_MODES = {"public", "private"}
|
VALID_MODES = {"public", "private"}
|
||||||
USER_MODE_PREFIX = "user:"
|
USER_MODE_PREFIX = "user:"
|
||||||
DEFAULT_WIDGETS = ["hero", "history", "strings", "status", "production", "comparison", "importStatus"]
|
DEFAULT_WIDGETS = ["hero", "history", "strings", "status", "production", "comparison", "importStatus"]
|
||||||
|
DEFAULT_HERO_METRICS = ["ac_power", "dc_power_total", "energy_today", "energy_total"]
|
||||||
|
DEFAULT_CHART_GROUPS = [{"id": "overview", "title": None, "metric_ids": ["ac_power", "dc_power_total", "inverter_temp"]}]
|
||||||
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"}
|
||||||
VALID_ANALYTICS_RANGES = {"today", "yesterday", "7d", "30d", "90d", "365d", "custom"}
|
VALID_ANALYTICS_RANGES = {"today", "yesterday", "7d", "30d", "90d", "365d", "custom"}
|
||||||
@@ -42,10 +44,12 @@ class KioskSettingsService:
|
|||||||
return {
|
return {
|
||||||
"mode": mode,
|
"mode": mode,
|
||||||
"widgets": list(DEFAULT_WIDGETS),
|
"widgets": list(DEFAULT_WIDGETS),
|
||||||
|
"hero_metric_ids": list(DEFAULT_HERO_METRICS),
|
||||||
"realtime_range": self._default_realtime_range(),
|
"realtime_range": self._default_realtime_range(),
|
||||||
"analytics_range": self._default_analytics_range(),
|
"analytics_range": self._default_analytics_range(),
|
||||||
"analytics_bucket": self._default_analytics_bucket(),
|
"analytics_bucket": self._default_analytics_bucket(),
|
||||||
"compare_mode": self._default_compare_mode(),
|
"compare_mode": self._default_compare_mode(),
|
||||||
|
"chart_groups": self._normalize_chart_groups(None),
|
||||||
"updated_at": None,
|
"updated_at": None,
|
||||||
"updated_by": None,
|
"updated_by": None,
|
||||||
}
|
}
|
||||||
@@ -54,10 +58,12 @@ class KioskSettingsService:
|
|||||||
cleaned = {
|
cleaned = {
|
||||||
"mode": mode,
|
"mode": mode,
|
||||||
"widgets": self._normalize_widgets(payload.get("widgets")),
|
"widgets": self._normalize_widgets(payload.get("widgets")),
|
||||||
|
"hero_metric_ids": self._normalize_metric_ids(payload.get("hero_metric_ids"), DEFAULT_HERO_METRICS),
|
||||||
"realtime_range": self._normalize_realtime_range(payload.get("realtime_range")),
|
"realtime_range": self._normalize_realtime_range(payload.get("realtime_range")),
|
||||||
"analytics_range": self._normalize_analytics_range(payload.get("analytics_range")),
|
"analytics_range": self._normalize_analytics_range(payload.get("analytics_range")),
|
||||||
"analytics_bucket": self._normalize_bucket(payload.get("analytics_bucket")),
|
"analytics_bucket": self._normalize_bucket(payload.get("analytics_bucket")),
|
||||||
"compare_mode": self._normalize_compare_mode(payload.get("compare_mode")),
|
"compare_mode": self._normalize_compare_mode(payload.get("compare_mode")),
|
||||||
|
"chart_groups": self._normalize_chart_groups(payload.get("chart_groups")),
|
||||||
"updated_at": payload.get("updated_at"),
|
"updated_at": payload.get("updated_at"),
|
||||||
"updated_by": payload.get("updated_by"),
|
"updated_by": payload.get("updated_by"),
|
||||||
}
|
}
|
||||||
@@ -83,6 +89,16 @@ class KioskSettingsService:
|
|||||||
normalized.append(widget)
|
normalized.append(widget)
|
||||||
return normalized or list(DEFAULT_WIDGETS)
|
return normalized or list(DEFAULT_WIDGETS)
|
||||||
|
|
||||||
|
def _normalize_metric_ids(self, metric_ids: Any, defaults: list[str]) -> list[str]:
|
||||||
|
if not isinstance(metric_ids, list):
|
||||||
|
return list(defaults)
|
||||||
|
normalized: list[str] = []
|
||||||
|
for item in metric_ids:
|
||||||
|
value = str(item or "").strip()
|
||||||
|
if value and value not in normalized:
|
||||||
|
normalized.append(value)
|
||||||
|
return normalized[:12] or list(defaults)
|
||||||
|
|
||||||
def _normalize_realtime_range(self, value: Any) -> str:
|
def _normalize_realtime_range(self, value: Any) -> str:
|
||||||
normalized = str(value or self._default_realtime_range()).strip()
|
normalized = str(value or self._default_realtime_range()).strip()
|
||||||
return normalized if normalized in VALID_REALTIME_RANGES else self._default_realtime_range()
|
return normalized if normalized in VALID_REALTIME_RANGES else self._default_realtime_range()
|
||||||
@@ -99,6 +115,31 @@ class KioskSettingsService:
|
|||||||
normalized = str(value or self._default_compare_mode()).strip()
|
normalized = str(value or self._default_compare_mode()).strip()
|
||||||
return normalized if normalized in self.settings.analytics["compare_modes"] else self._default_compare_mode()
|
return normalized if normalized in self.settings.analytics["compare_modes"] else self._default_compare_mode()
|
||||||
|
|
||||||
|
def _normalize_chart_groups(self, groups: Any) -> list[dict[str, Any]]:
|
||||||
|
if not isinstance(groups, list):
|
||||||
|
return [dict(item) for item in DEFAULT_CHART_GROUPS]
|
||||||
|
|
||||||
|
normalized: list[dict[str, Any]] = []
|
||||||
|
for index, item in enumerate(groups):
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
continue
|
||||||
|
raw_metrics = item.get("metric_ids")
|
||||||
|
if not isinstance(raw_metrics, list):
|
||||||
|
continue
|
||||||
|
metric_ids: list[str] = []
|
||||||
|
for metric in raw_metrics:
|
||||||
|
value = str(metric or "").strip()
|
||||||
|
if value and value not in metric_ids:
|
||||||
|
metric_ids.append(value)
|
||||||
|
if not metric_ids:
|
||||||
|
continue
|
||||||
|
chart_id = str(item.get("id") or f"chart_{index + 1}").strip() or f"chart_{index + 1}"
|
||||||
|
title_raw = item.get("title")
|
||||||
|
title = None if title_raw is None else str(title_raw).strip() or None
|
||||||
|
normalized.append({"id": chart_id[:80], "title": title, "metric_ids": metric_ids[:24]})
|
||||||
|
|
||||||
|
return normalized[:8] or [dict(item) for item in DEFAULT_CHART_GROUPS]
|
||||||
|
|
||||||
def _default_realtime_range(self) -> str:
|
def _default_realtime_range(self) -> str:
|
||||||
raw = str(self.settings.realtime.get("history_default_range", "12h"))
|
raw = str(self.settings.realtime.get("history_default_range", "12h"))
|
||||||
return raw if raw in VALID_REALTIME_RANGES else "12h"
|
return raw if raw in VALID_REALTIME_RANGES else "12h"
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -4,6 +4,7 @@ import type {
|
|||||||
AuthUsersPayload,
|
AuthUsersPayload,
|
||||||
DashboardConfig,
|
DashboardConfig,
|
||||||
DiagnosticsPayload,
|
DiagnosticsPayload,
|
||||||
|
KioskChartGroup,
|
||||||
KioskSettingsPayload,
|
KioskSettingsPayload,
|
||||||
DistributionPayload,
|
DistributionPayload,
|
||||||
HistoryPayload,
|
HistoryPayload,
|
||||||
@@ -68,9 +69,13 @@ async function demoResponse<T>(factory: () => T): Promise<T> {
|
|||||||
return clone(factory());
|
return clone(factory());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function demoKioskChartGroups(): KioskChartGroup[] {
|
||||||
|
return [{ id: "overview", title: null, metric_ids: ["ac_power", "dc_power_total", "inverter_temp"] }];
|
||||||
|
}
|
||||||
|
|
||||||
export const api = {
|
export const api = {
|
||||||
getConfig: () => (DEMO_MODE ? demoResponse(() => demoConfig) : request<DashboardConfig>("/dashboard/config")),
|
getConfig: () => (DEMO_MODE ? demoResponse(() => demoConfig) : request<DashboardConfig>("/dashboard/config")),
|
||||||
getKioskSettings: (mode: "public" | "private") => (DEMO_MODE ? demoResponse(() => ({ mode, widgets: ["hero", "history", "strings", "status", "production", "comparison", "importStatus"], realtime_range: "today", analytics_range: "30d", analytics_bucket: "day", compare_mode: "none" })) : request<KioskSettingsPayload>(`/dashboard/kiosk-settings?mode=${mode}`)),
|
getKioskSettings: (mode: "public" | "private") => (DEMO_MODE ? demoResponse(() => ({ mode, widgets: ["hero", "history", "strings", "status", "production", "comparison", "importStatus"], hero_metric_ids: ["ac_power", "dc_power_total", "energy_today", "energy_total"], realtime_range: "today", analytics_range: "30d", analytics_bucket: "day", compare_mode: "none", chart_groups: demoKioskChartGroups() })) : request<KioskSettingsPayload>(`/dashboard/kiosk-settings?mode=${mode}`)),
|
||||||
saveKioskSettings: (payload: KioskSettingsPayload) => (DEMO_MODE ? demoResponse(() => payload) : request<KioskSettingsPayload>("/dashboard/kiosk-settings", { method: "PUT", body: JSON.stringify(payload) })),
|
saveKioskSettings: (payload: KioskSettingsPayload) => (DEMO_MODE ? demoResponse(() => payload) : request<KioskSettingsPayload>("/dashboard/kiosk-settings", { method: "PUT", body: JSON.stringify(payload) })),
|
||||||
getAuthStatus: () => (DEMO_MODE ? demoResponse(() => demoAuthStatus) : request<AuthStatus>("/auth/status")),
|
getAuthStatus: () => (DEMO_MODE ? demoResponse(() => demoAuthStatus) : request<AuthStatus>("/auth/status")),
|
||||||
login: (username: string, password: string) =>
|
login: (username: string, password: string) =>
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export const demoConfig: DashboardConfig = {
|
|||||||
{ metric_id: "string_1_voltage", label: "Napiecie stringu DC1", entity_id: "sofarsolar_dc1_voltage", measurement: "V", unit: "V", kind: "gauge" },
|
{ metric_id: "string_1_voltage", label: "Napiecie stringu DC1", entity_id: "sofarsolar_dc1_voltage", measurement: "V", unit: "V", kind: "gauge" },
|
||||||
{ metric_id: "string_2_power", label: "Moc stringu DC2", entity_id: "sofarsolar_dc2_power", measurement: "W", unit: "W", kind: "gauge" },
|
{ metric_id: "string_2_power", label: "Moc stringu DC2", entity_id: "sofarsolar_dc2_power", measurement: "W", unit: "W", kind: "gauge" },
|
||||||
{ metric_id: "string_2_voltage", label: "Napiecie stringu DC2", entity_id: "sofarsolar_dc2_voltage", measurement: "V", unit: "V", kind: "gauge" },
|
{ metric_id: "string_2_voltage", label: "Napiecie stringu DC2", entity_id: "sofarsolar_dc2_voltage", measurement: "V", unit: "V", kind: "gauge" },
|
||||||
{ metric_id: "inverter_temperature", label: "Temperatura falownika", entity_id: "sofarsolar_temprature_inverter", measurement: "°C", unit: "°C", kind: "gauge" },
|
{ metric_id: "inverter_temp", label: "Temperatura falownika", entity_id: "sofarsolar_temprature_inverter", measurement: "°C", unit: "°C", kind: "gauge" },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -77,9 +77,9 @@ export const demoSnapshot = (): SnapshotPayload => ({
|
|||||||
hero_cards: [
|
hero_cards: [
|
||||||
{ metric_id: "ac_power", label: "Produkcja AC", value: 6840, unit: "W", accent: "emerald", subtitle: "Aktualna moc oddawana przez falownik" },
|
{ metric_id: "ac_power", label: "Produkcja AC", value: 6840, unit: "W", accent: "emerald", subtitle: "Aktualna moc oddawana przez falownik" },
|
||||||
{ metric_id: "energy_today", label: "Dzisiaj", value: 31.8, unit: "kWh", accent: "amber", subtitle: "Liczone z energy_total / fallback z AC power" },
|
{ metric_id: "energy_today", label: "Dzisiaj", value: 31.8, unit: "kWh", accent: "amber", subtitle: "Liczone z energy_total / fallback z AC power" },
|
||||||
{ metric_id: "dc1_power", label: "String DC1", value: 3450, unit: "W", accent: "emerald", subtitle: "Wschod" },
|
{ metric_id: "string_1_power", label: "String DC1", value: 3450, unit: "W", accent: "emerald", subtitle: "Wschód" },
|
||||||
{ metric_id: "dc2_power", label: "String DC2", value: 3310, unit: "W", accent: "emerald", subtitle: "Zachod" },
|
{ metric_id: "string_2_power", label: "String DC2", value: 3310, unit: "W", accent: "emerald", subtitle: "Zachód" },
|
||||||
{ metric_id: "inverter_temperature", label: "Temp. falownika", value: 47.3, unit: "°C", accent: "rose", subtitle: "Live status termiczny" },
|
{ metric_id: "inverter_temp", label: "Temp. falownika", value: 47.3, unit: "°C", accent: "rose", subtitle: "Live status termiczny" },
|
||||||
],
|
],
|
||||||
kpis: {
|
kpis: {
|
||||||
energy_today: { metric_id: "energy_today", label: "Energia dzis", unit: "kWh", value: 31.8, precision: 2, kind: "counter", status: "ok" },
|
energy_today: { metric_id: "energy_today", label: "Energia dzis", unit: "kWh", value: 31.8, precision: 2, kind: "counter", status: "ok" },
|
||||||
@@ -94,7 +94,7 @@ export const demoSnapshot = (): SnapshotPayload => ({
|
|||||||
],
|
],
|
||||||
phases: [],
|
phases: [],
|
||||||
status: [
|
status: [
|
||||||
{ metric_id: "inverter_temperature", label: "Temperatura falownika", unit: "°C", value: 47.3, precision: 1, kind: "gauge", status: "ok" },
|
{ metric_id: "inverter_temp", label: "Temperatura falownika", unit: "°C", value: 47.3, precision: 1, kind: "gauge", status: "ok" },
|
||||||
{ metric_id: "data_freshness", label: "Swiezosc danych", unit: "", value: "3 s temu", precision: 0, kind: "text", status: "ok" },
|
{ metric_id: "data_freshness", label: "Swiezosc danych", unit: "", value: "3 s temu", precision: 0, kind: "text", status: "ok" },
|
||||||
],
|
],
|
||||||
faults: [],
|
faults: [],
|
||||||
@@ -110,9 +110,9 @@ export const demoHistory: HistoryPayload = {
|
|||||||
end: isoAt(0),
|
end: isoAt(0),
|
||||||
series: [
|
series: [
|
||||||
{ metric_id: "ac_power", label: "Moc AC", unit: "W", points: historyPoints([0, 120, 860, 1840, 2760, 3920, 5180, 6020, 6840, 6500, 5710, 4980]) },
|
{ metric_id: "ac_power", label: "Moc AC", unit: "W", points: historyPoints([0, 120, 860, 1840, 2760, 3920, 5180, 6020, 6840, 6500, 5710, 4980]) },
|
||||||
{ metric_id: "dc1_power", label: "DC1", unit: "W", points: historyPoints([0, 80, 620, 1320, 2140, 2860, 3250, 3490, 3450, 3300, 2920, 2480]) },
|
{ metric_id: "string_1_power", label: "DC1", unit: "W", points: historyPoints([0, 80, 620, 1320, 2140, 2860, 3250, 3490, 3450, 3300, 2920, 2480]) },
|
||||||
{ metric_id: "dc2_power", label: "DC2", unit: "W", points: historyPoints([0, 40, 240, 520, 880, 1260, 1930, 2530, 3310, 3200, 2790, 2410]) },
|
{ metric_id: "string_2_power", label: "DC2", unit: "W", points: historyPoints([0, 40, 240, 520, 880, 1260, 1930, 2530, 3310, 3200, 2790, 2410]) },
|
||||||
{ metric_id: "inverter_temperature", label: "Temp. falownika", unit: "°C", points: historyPoints([22, 24, 27, 31, 35, 39, 42, 45, 47.3, 46.8, 44.1, 41.2]) },
|
{ metric_id: "inverter_temp", label: "Temp. falownika", unit: "°C", points: historyPoints([22, 24, 27, 31, 35, 39, 42, 45, 47.3, 46.8, 44.1, 41.2]) },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ const messages = {
|
|||||||
settingsSubtitle: "Import archiwum, wygląd, kiosk, bezpieczeństwo",
|
settingsSubtitle: "Import archiwum, wygląd, kiosk, bezpieczeństwo",
|
||||||
noData: "Brak danych",
|
noData: "Brak danych",
|
||||||
noDataDescription: "Brak odpowiedzi z backendu lub InfluxDB.",
|
noDataDescription: "Brak odpowiedzi z backendu lub InfluxDB.",
|
||||||
chartPowerHistory: "Historia mocy i temperatury",
|
kioskCharts: "Wykresy",
|
||||||
chartProduction: "Produkcja",
|
chartProduction: "Produkcja",
|
||||||
chartProductionSubtitle: "Agregacja w wybranym bucketcie",
|
chartProductionSubtitle: "Agregacja w wybranym bucketcie",
|
||||||
chartComparison: "Porównanie okresów",
|
chartComparison: "Porównanie okresów",
|
||||||
@@ -141,7 +141,7 @@ const messages = {
|
|||||||
settingsSubtitle: "Archive import, appearance, kiosk, security",
|
settingsSubtitle: "Archive import, appearance, kiosk, security",
|
||||||
noData: "No data",
|
noData: "No data",
|
||||||
noDataDescription: "No response from backend or InfluxDB.",
|
noDataDescription: "No response from backend or InfluxDB.",
|
||||||
chartPowerHistory: "Power and temperature history",
|
kioskCharts: "Charts",
|
||||||
chartProduction: "Production",
|
chartProduction: "Production",
|
||||||
chartProductionSubtitle: "Aggregated by selected bucket",
|
chartProductionSubtitle: "Aggregated by selected bucket",
|
||||||
chartComparison: "Period comparison",
|
chartComparison: "Period comparison",
|
||||||
|
|||||||
@@ -263,13 +263,21 @@ export interface AuthUsersPayload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface KioskChartGroup {
|
||||||
|
id: string;
|
||||||
|
title?: string | null;
|
||||||
|
metric_ids: string[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface KioskSettingsPayload {
|
export interface KioskSettingsPayload {
|
||||||
mode: "public" | "private";
|
mode: "public" | "private";
|
||||||
widgets: string[];
|
widgets: string[];
|
||||||
|
hero_metric_ids: string[];
|
||||||
realtime_range: string;
|
realtime_range: string;
|
||||||
analytics_range: string;
|
analytics_range: string;
|
||||||
analytics_bucket: string;
|
analytics_bucket: string;
|
||||||
compare_mode: string;
|
compare_mode: string;
|
||||||
|
chart_groups: KioskChartGroup[];
|
||||||
updated_at?: string | null;
|
updated_at?: string | null;
|
||||||
updated_by?: string | null;
|
updated_by?: string | null;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user