poprawki i zmiany ux
This commit is contained in:
@@ -55,9 +55,9 @@ class AuthService:
|
||||
self._login_legacy_user(username, password)
|
||||
else:
|
||||
if not user.is_active:
|
||||
raise ValueError("Konto jest nieaktywne")
|
||||
raise ValueError("Account is inactive")
|
||||
if not check_password_hash(user.password_hash, password):
|
||||
raise ValueError("Niepoprawny login lub haslo")
|
||||
raise ValueError("Invalid username or password")
|
||||
self._set_session(user.username, user.display_name, user.role)
|
||||
|
||||
return self.status()
|
||||
@@ -84,7 +84,7 @@ class AuthService:
|
||||
if not self.enabled:
|
||||
return
|
||||
if session.get(SESSION_ROLE_KEY) != "admin":
|
||||
raise PermissionError("Brak uprawnien administratora")
|
||||
raise PermissionError("Administrator permissions are required")
|
||||
|
||||
def configure_app(self, app) -> None:
|
||||
max_age = int(self.settings.auth["session_max_age_seconds"])
|
||||
@@ -101,7 +101,7 @@ class AuthService:
|
||||
clean_password = self._validate_password(password)
|
||||
resolved_display_name = (display_name or normalized_username).strip()
|
||||
if not resolved_display_name:
|
||||
raise ValueError("Display name nie moze byc pusty")
|
||||
raise ValueError("Display name cannot be empty")
|
||||
return self.user_repository.upsert_user(
|
||||
username=normalized_username,
|
||||
password_hash=generate_password_hash(clean_password),
|
||||
@@ -118,16 +118,33 @@ class AuthService:
|
||||
generate_password_hash(clean_password),
|
||||
)
|
||||
if user is None:
|
||||
raise ValueError(f"Uzytkownik '{normalized_username}' nie istnieje")
|
||||
raise ValueError(f"User '{normalized_username}' does not exist")
|
||||
return user
|
||||
|
||||
def update_role(self, *, username: str, role: str) -> AuthUser:
|
||||
normalized_username = self._normalize_username(username)
|
||||
normalized_role = self._normalize_role(role)
|
||||
user = self.user_repository.get_by_username(normalized_username)
|
||||
if user is None:
|
||||
raise ValueError(f"User '{normalized_username}' does not exist")
|
||||
if user.role == normalized_role:
|
||||
return user
|
||||
if user.role == 'admin' and normalized_role != 'admin' and self.user_repository.count_admin_users() <= 1:
|
||||
raise ValueError('At least one active admin user must remain')
|
||||
updated = self.user_repository.update_role(normalized_username, normalized_role)
|
||||
if updated is None:
|
||||
raise ValueError(f"User '{normalized_username}' does not exist")
|
||||
if session.get(SESSION_USER_KEY) == updated.username:
|
||||
session[SESSION_ROLE_KEY] = updated.role
|
||||
return updated
|
||||
|
||||
def _login_legacy_user(self, username: str, password: str) -> None:
|
||||
expected_username = self.settings.auth["username"]
|
||||
expected_password = self.settings.auth["password"]
|
||||
expected_password_hash = self.settings.auth.get("password_hash")
|
||||
|
||||
if username != expected_username:
|
||||
raise ValueError("Niepoprawny login lub haslo")
|
||||
raise ValueError("Invalid username or password")
|
||||
|
||||
if expected_password_hash:
|
||||
password_ok = check_password_hash(expected_password_hash, password)
|
||||
@@ -135,7 +152,7 @@ class AuthService:
|
||||
password_ok = password == expected_password
|
||||
|
||||
if not password_ok:
|
||||
raise ValueError("Niepoprawny login lub haslo")
|
||||
raise ValueError("Invalid username or password")
|
||||
|
||||
self._set_session(
|
||||
expected_username,
|
||||
@@ -153,19 +170,19 @@ class AuthService:
|
||||
def _normalize_username(self, username: str) -> str:
|
||||
normalized = (username or "").strip()
|
||||
if not normalized:
|
||||
raise ValueError("Username nie moze byc pusty")
|
||||
raise ValueError("Username cannot be empty")
|
||||
return normalized
|
||||
|
||||
def _normalize_role(self, role: str) -> str:
|
||||
normalized = (role or "").strip().lower()
|
||||
if normalized not in VALID_ROLES:
|
||||
raise ValueError("Rola musi byc jedna z: admin, user")
|
||||
raise ValueError("Role must be one of: admin, user")
|
||||
return normalized
|
||||
|
||||
def _validate_password(self, password: str) -> str:
|
||||
clean_password = password or ""
|
||||
if len(clean_password) < 8:
|
||||
raise ValueError("Haslo musi miec co najmniej 8 znakow")
|
||||
raise ValueError("Password must be at least 8 characters long")
|
||||
return clean_password
|
||||
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ class HistoricalSyncService:
|
||||
with self._state_lock:
|
||||
self._state.running = False
|
||||
self._state.state = "idle"
|
||||
self._state.message = "Brak brakujacych dni do importu."
|
||||
self._state.message = "There are no missing days to import."
|
||||
self._state.finished_at = datetime.utcnow()
|
||||
self._refresh_coverage(lock_held=True)
|
||||
self._refresh_available_bounds(lock_held=True)
|
||||
@@ -92,7 +92,7 @@ class HistoricalSyncService:
|
||||
resolved_start, resolved_end = resolved
|
||||
total_days = (resolved_end - resolved_start).days + 1
|
||||
total_chunks = max(ceil(total_days / chunk_days), 1)
|
||||
start_message = "Start importu archiwalnego" if not auto else "Start automatycznej synchronizacji archiwum"
|
||||
start_message = "Historical import started" if not auto else "Automatic historical sync started"
|
||||
|
||||
with self._state_lock:
|
||||
if self._worker and self._worker.is_alive():
|
||||
@@ -138,18 +138,18 @@ class HistoricalSyncService:
|
||||
|
||||
self._record_event(
|
||||
level="info",
|
||||
title="Uruchomiono zadanie",
|
||||
message=f"Zakres {resolved_start.isoformat()} -> {resolved_end.isoformat()}, chunk {chunk_days} dni",
|
||||
title="Job started",
|
||||
message=f"Range {resolved_start.isoformat()} -> {resolved_end.isoformat()}, chunk size {chunk_days} days",
|
||||
)
|
||||
return self.status()
|
||||
|
||||
def cancel(self) -> HistoricalImportStatus:
|
||||
self._cancel_event.set()
|
||||
with self._state_lock:
|
||||
self._state.message = "Anulowanie zadania..."
|
||||
self._state.message = "Cancelling job..."
|
||||
self._refresh_runtime_metrics(lock_held=True)
|
||||
snapshot = copy.deepcopy(self._state)
|
||||
self._record_event(level="warn", title="Anulowanie", message="Uzytkownik poprosil o zatrzymanie zadania.")
|
||||
self._record_event(level="warn", title="Cancellation requested", message="The user requested the job to stop.")
|
||||
return snapshot
|
||||
|
||||
def run_blocking(
|
||||
@@ -185,8 +185,8 @@ class HistoricalSyncService:
|
||||
)
|
||||
self._record_event(
|
||||
level="info",
|
||||
title="Uruchomiono zadanie",
|
||||
message=f"Zakres {resolved_start.isoformat()} -> {resolved_end.isoformat()}, chunk {chunk_days} dni",
|
||||
title="Job started",
|
||||
message=f"Range {resolved_start.isoformat()} -> {resolved_end.isoformat()}, chunk size {chunk_days} days",
|
||||
)
|
||||
self._run_worker(
|
||||
start_date=resolved_start,
|
||||
@@ -239,8 +239,8 @@ class HistoricalSyncService:
|
||||
chunk_start = start_date
|
||||
while chunk_start <= end_date:
|
||||
if self._cancel_event.is_set():
|
||||
self._record_event(level="warn", title="Anulowano", message="Import archiwalny anulowany przez uzytkownika.")
|
||||
self._finish("cancelled", running=False, message="Import archiwalny anulowany przez uzytkownika.")
|
||||
self._record_event(level="warn", title="Cancelled", message="Historical import was cancelled by the user.")
|
||||
self._finish("cancelled", running=False, message="Historical import was cancelled by the user.")
|
||||
return
|
||||
|
||||
chunk_index += 1
|
||||
@@ -259,10 +259,10 @@ class HistoricalSyncService:
|
||||
skipped_days=skipped,
|
||||
energy_kwh=energy_kwh,
|
||||
state="cancelled",
|
||||
note="Chunk zatrzymany podczas przetwarzania",
|
||||
note="Chunk stopped during processing",
|
||||
)
|
||||
self._record_event(level="warn", title="Anulowano", message="Import archiwalny anulowany przez uzytkownika.")
|
||||
self._finish("cancelled", running=False, message="Import archiwalny anulowany przez uzytkownika.")
|
||||
self._record_event(level="warn", title="Cancelled", message="Historical import was cancelled by the user.")
|
||||
self._finish("cancelled", running=False, message="Historical import was cancelled by the user.")
|
||||
return
|
||||
|
||||
self._close_chunk(
|
||||
@@ -271,23 +271,23 @@ class HistoricalSyncService:
|
||||
skipped_days=skipped,
|
||||
energy_kwh=energy_kwh,
|
||||
state="completed",
|
||||
note=f"Chunk zakonczony: import {imported}, pominiete {skipped}",
|
||||
note=f"Chunk completed: imported {imported}, skipped {skipped}",
|
||||
)
|
||||
self._record_event(
|
||||
level="success",
|
||||
title=f"Chunk {chunk_index}/{total_chunks} zakonczony",
|
||||
message=f"Zakres {chunk_start.isoformat()} -> {chunk_end.isoformat()}, import {imported}, pominiete {skipped}, energia {energy_kwh:.2f} kWh",
|
||||
title=f"Chunk {chunk_index}/{total_chunks} completed",
|
||||
message=f"Range {chunk_start.isoformat()} -> {chunk_end.isoformat()}, imported {imported}, skipped {skipped}, energy {energy_kwh:.2f} kWh",
|
||||
chunk_index=chunk_index,
|
||||
)
|
||||
chunk_start = chunk_end + timedelta(days=1)
|
||||
|
||||
final_message = "Synchronizacja archiwalna zakonczona" if auto else "Import archiwalny zakonczony"
|
||||
self._record_event(level="success", title="Zakonczono", message=final_message)
|
||||
final_message = "Historical synchronization completed" if auto else "Historical import completed"
|
||||
self._record_event(level="success", title="Completed", message=final_message)
|
||||
self._finish("completed", running=False, message=final_message)
|
||||
except Exception as exc:
|
||||
logger.exception("Historical import failed")
|
||||
self._record_event(level="error", title="Blad importu", message=str(exc))
|
||||
self._finish("failed", running=False, message="Import archiwalny zakonczyl sie bledem.", last_error=str(exc))
|
||||
self._record_event(level="error", title="Import error", message=str(exc))
|
||||
self._finish("failed", running=False, message="Historical import finished with an error.", last_error=str(exc))
|
||||
|
||||
def _process_chunk(self, *, chunk_index: int, start_day: date, end_day: date, force: bool) -> tuple[int, int, float, bool]:
|
||||
imported_days = 0
|
||||
@@ -303,9 +303,9 @@ class HistoricalSyncService:
|
||||
self._advance_day(
|
||||
day,
|
||||
imported=False,
|
||||
message=f"Pominieto {day.isoformat()} - dzien juz istnieje w cache",
|
||||
message=f"Skipped {day.isoformat()} - day already exists in cache",
|
||||
level="warn",
|
||||
title="Pominieto dzien",
|
||||
title="Day skipped",
|
||||
chunk_index=chunk_index,
|
||||
)
|
||||
continue
|
||||
@@ -316,9 +316,9 @@ class HistoricalSyncService:
|
||||
self._advance_day(
|
||||
day,
|
||||
imported=False,
|
||||
message=f"Pominieto {day.isoformat()} - brak probek w InfluxDB",
|
||||
message=f"Skipped {day.isoformat()} - no samples in InfluxDB",
|
||||
level="warn",
|
||||
title="Brak probek",
|
||||
title="No samples",
|
||||
chunk_index=chunk_index,
|
||||
)
|
||||
continue
|
||||
@@ -336,9 +336,9 @@ class HistoricalSyncService:
|
||||
self._advance_day(
|
||||
day,
|
||||
imported=True,
|
||||
message=f"Zaimportowano {day.isoformat()} ({total:.2f} kWh)",
|
||||
message=f"Imported {day.isoformat()} ({total:.2f} kWh)",
|
||||
level="success",
|
||||
title="Zaimportowano dzien",
|
||||
title="Day imported",
|
||||
chunk_index=chunk_index,
|
||||
energy_kwh=total,
|
||||
)
|
||||
@@ -366,7 +366,7 @@ class HistoricalSyncService:
|
||||
self._state.message = message
|
||||
self._refresh_coverage(lock_held=True)
|
||||
self._refresh_runtime_metrics(lock_held=True)
|
||||
suffix = f" Energia: {energy_kwh:.2f} kWh." if imported and energy_kwh is not None else ""
|
||||
suffix = f" Energy: {energy_kwh:.2f} kWh." if imported and energy_kwh is not None else ""
|
||||
self._record_event(
|
||||
level=level,
|
||||
title=title,
|
||||
@@ -383,19 +383,19 @@ class HistoricalSyncService:
|
||||
end_date=chunk_end,
|
||||
state="running",
|
||||
started_at=datetime.utcnow(),
|
||||
note=f"Aktywny chunk {chunk_start.isoformat()} -> {chunk_end.isoformat()}",
|
||||
note=f"Active chunk {chunk_start.isoformat()} -> {chunk_end.isoformat()}",
|
||||
)
|
||||
with self._state_lock:
|
||||
self._state.current_chunk_start = chunk_start
|
||||
self._state.current_chunk_end = chunk_end
|
||||
self._state.active_chunk_index = chunk_index
|
||||
self._state.message = f"Przetwarzanie zakresu {chunk_start.isoformat()} -> {chunk_end.isoformat()}"
|
||||
self._state.message = f"Processing range {chunk_start.isoformat()} -> {chunk_end.isoformat()}"
|
||||
self._upsert_chunk_locked(chunk)
|
||||
self._refresh_runtime_metrics(lock_held=True)
|
||||
self._record_event(
|
||||
level="info",
|
||||
title=f"Chunk {chunk_index}/{total_chunks}",
|
||||
message=f"Start zakresu {chunk_start.isoformat()} -> {chunk_end.isoformat()}",
|
||||
message=f"Starting range {chunk_start.isoformat()} -> {chunk_end.isoformat()}",
|
||||
chunk_index=chunk_index,
|
||||
)
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ class KioskSettingsService:
|
||||
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")
|
||||
raise ValueError("Mode must be one of: public, private")
|
||||
|
||||
def _normalize_widgets(self, widgets: Any) -> list[str]:
|
||||
if not isinstance(widgets, list):
|
||||
|
||||
@@ -50,34 +50,34 @@ class RealtimeService:
|
||||
inverter_temp = to_float(_value(latest, "inverter_temp"))
|
||||
|
||||
hero_cards = [
|
||||
self._hero_card("ac_power", ac_power, subtitle="Aktualna moc AC"),
|
||||
self._hero_card("dc_power_total", total_dc_power, label="Moc DC laczna", unit="W", subtitle="Suma stringow DC"),
|
||||
self._hero_card("energy_today", energy_today, label="Energia dzis", unit="kWh", subtitle="Liczona z danych Influx"),
|
||||
self._hero_card("energy_total", total_energy, label="Energia laczna", unit="kWh", subtitle="Licznik calkowity"),
|
||||
self._hero_card("ac_power", ac_power, subtitle="Current AC power"),
|
||||
self._hero_card("dc_power_total", total_dc_power, label="Total DC power", unit="W", subtitle="Sum of DC strings"),
|
||||
self._hero_card("energy_today", energy_today, label="Energy today", unit="kWh", subtitle="Calculated from Influx data"),
|
||||
self._hero_card("energy_total", total_energy, label="Total energy", unit="kWh", subtitle="Lifetime counter"),
|
||||
]
|
||||
if inverter_temp is not None:
|
||||
hero_cards.append(self._hero_card("inverter_temp", inverter_temp, label="Temp. falownika", unit="°C", subtitle="Sensor opcjonalny"))
|
||||
hero_cards.append(self._hero_card("inverter_temp", inverter_temp, label="Inverter temperature", unit="°C", subtitle="Optional sensor"))
|
||||
|
||||
kpis = {
|
||||
"energy_today": custom_metric_value("energy_today", "Energia dzis", energy_today, unit="kWh", precision=2, status="ok"),
|
||||
"energy_yesterday": custom_metric_value("energy_yesterday", "Energia wczoraj", energy_yesterday, unit="kWh", precision=2, status="ok"),
|
||||
"energy_today": custom_metric_value("energy_today", "Energy today", energy_today, unit="kWh", precision=2, status="ok"),
|
||||
"energy_yesterday": custom_metric_value("energy_yesterday", "Energy yesterday", energy_yesterday, unit="kWh", precision=2, status="ok"),
|
||||
"energy_total": custom_metric_value(
|
||||
"energy_total",
|
||||
"Energia laczna",
|
||||
"Total energy",
|
||||
total_energy,
|
||||
unit="kWh",
|
||||
precision=2,
|
||||
timestamp=_timestamp(latest, "energy_total"),
|
||||
status="ok",
|
||||
),
|
||||
"dc_power_total": custom_metric_value("dc_power_total", "Moc DC laczna", total_dc_power, unit="W", precision=0, status="ok"),
|
||||
"dc_power_total": custom_metric_value("dc_power_total", "Total DC power", total_dc_power, unit="W", precision=0, status="ok"),
|
||||
}
|
||||
|
||||
comparison = compare_delta_pct(energy_today, energy_yesterday)
|
||||
if comparison is not None:
|
||||
kpis["today_vs_yesterday"] = custom_metric_value(
|
||||
"today_vs_yesterday",
|
||||
"Dzis vs wczoraj",
|
||||
"Today vs yesterday",
|
||||
comparison,
|
||||
unit="%",
|
||||
precision=2,
|
||||
@@ -97,7 +97,7 @@ class RealtimeService:
|
||||
status.append(
|
||||
custom_metric_value(
|
||||
"data_refresh",
|
||||
"Ostatni odczyt energii",
|
||||
"Last energy reading",
|
||||
_timestamp(latest, "energy_total").isoformat() if _timestamp(latest, "energy_total") else None,
|
||||
status="ok" if _timestamp(latest, "energy_total") else "neutral",
|
||||
kind="text",
|
||||
@@ -144,7 +144,7 @@ class RealtimeService:
|
||||
series.append(
|
||||
{
|
||||
"metric_id": metric.id,
|
||||
"label": metric.label if slot != "power" else group["label"],
|
||||
"label": f"{group['label']} power" if slot == "power" else f"{group['label']} voltage" if slot == "voltage" else metric.label,
|
||||
"unit": metric.unit,
|
||||
"points": self.influx.gauge_history(metric, window.start, window.end, interval=interval, aggregate="mean"),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user