poprawki i zmiany ux
This commit is contained in:
@@ -101,7 +101,8 @@ def create_app() -> Flask:
|
||||
|
||||
@app.after_request
|
||||
def append_cors_headers(response):
|
||||
return _apply_cors(response)
|
||||
response = _apply_cors(response)
|
||||
return _apply_cache_headers(response)
|
||||
|
||||
@app.errorhandler(HTTPException)
|
||||
def handle_http_exception(exc: HTTPException):
|
||||
@@ -123,7 +124,7 @@ def _register_cli_commands(app: Flask) -> None:
|
||||
auth_service = get_auth_service()
|
||||
|
||||
@app.cli.command("create-admin")
|
||||
@click.option("--username", required=True, help="Login")
|
||||
@click.option("--username", required=True, help="Username")
|
||||
@click.option("--password", required=True, hide_input=True, confirmation_prompt=True, help="Password")
|
||||
@click.option("--display-name", default=None, help="Name")
|
||||
def create_admin_command(username: str, password: str, display_name: str | None):
|
||||
@@ -134,14 +135,14 @@ def _register_cli_commands(app: Flask) -> None:
|
||||
role="admin",
|
||||
display_name=display_name,
|
||||
)
|
||||
click.echo(f"Utworzono konto admina: {user.username}")
|
||||
click.echo(f"Admin account created: {user.username}")
|
||||
except ValueError as exc:
|
||||
raise click.ClickException(str(exc)) from exc
|
||||
except Exception as exc: # pragma: no cover
|
||||
raise click.ClickException(f"Cant create admin account: {exc}") from exc
|
||||
raise click.ClickException(f"Cannot create admin account: {exc}") from exc
|
||||
|
||||
@app.cli.command("create-user")
|
||||
@click.option("--username", required=True, help="Login")
|
||||
@click.option("--username", required=True, help="Username")
|
||||
@click.option("--password", required=True, hide_input=True, confirmation_prompt=True, help="Password")
|
||||
@click.option("--display-name", default=None, help="Name")
|
||||
def create_user_command(username: str, password: str, display_name: str | None):
|
||||
@@ -152,23 +153,23 @@ def _register_cli_commands(app: Flask) -> None:
|
||||
role="user",
|
||||
display_name=display_name,
|
||||
)
|
||||
click.echo(f"Admin created: {user.username}")
|
||||
click.echo(f"User account created: {user.username}")
|
||||
except ValueError as exc:
|
||||
raise click.ClickException(str(exc)) from exc
|
||||
except Exception as exc: # pragma: no cover
|
||||
raise click.ClickException(f"Cant create user account: {exc}") from exc
|
||||
raise click.ClickException(f"Cannot create user account: {exc}") from exc
|
||||
|
||||
@app.cli.command("reset-password")
|
||||
@click.option("--username", required=True, help="Login")
|
||||
@click.option("--username", required=True, help="Username")
|
||||
@click.option("--password", required=True, hide_input=True, confirmation_prompt=True, help="Password")
|
||||
def reset_password_command(username: str, password: str):
|
||||
try:
|
||||
user = auth_service.reset_password(username=username, new_password=password)
|
||||
click.echo(f"Passowrd reseted for: {user.username}")
|
||||
click.echo(f"Password reset for: {user.username}")
|
||||
except ValueError as exc:
|
||||
raise click.ClickException(str(exc)) from exc
|
||||
except Exception as exc: # pragma: no cover
|
||||
raise click.ClickException(f"Can't password reset: {exc}") from exc
|
||||
raise click.ClickException(f"Cannot reset password: {exc}") from exc
|
||||
|
||||
|
||||
def _bootstrap_background_services(debug: bool) -> None:
|
||||
@@ -191,3 +192,11 @@ def _apply_cors(response):
|
||||
response.headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS, PUT"
|
||||
response.headers["Access-Control-Allow-Credentials"] = "true"
|
||||
return response
|
||||
|
||||
|
||||
def _apply_cache_headers(response):
|
||||
if request.path == "/" or request.path.startswith("/api/"):
|
||||
response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, proxy-revalidate"
|
||||
response.headers["Pragma"] = "no-cache"
|
||||
response.headers["Expires"] = "0"
|
||||
return response
|
||||
|
||||
@@ -78,3 +78,21 @@ def reset_password(username: str):
|
||||
return jsonify({"detail": str(exc)}), 403
|
||||
except ValueError as exc:
|
||||
return jsonify({"detail": str(exc)}), 400
|
||||
|
||||
|
||||
@auth_blueprint.put("/auth/users/<username>/role")
|
||||
def update_user_role(username: str):
|
||||
payload = request.get_json(silent=True) or {}
|
||||
try:
|
||||
service.require_admin()
|
||||
user = service.update_role(username=username, role=payload.get("role", "user"))
|
||||
return jsonify(to_plain({
|
||||
"username": user.username,
|
||||
"display_name": user.display_name,
|
||||
"role": user.role,
|
||||
"is_active": user.is_active,
|
||||
}))
|
||||
except PermissionError as exc:
|
||||
return jsonify({"detail": str(exc)}), 403
|
||||
except ValueError as exc:
|
||||
return jsonify({"detail": str(exc)}), 400
|
||||
|
||||
@@ -25,11 +25,11 @@ def _resolve_kiosk_mode(requested_mode: str, require_write_access: bool = False)
|
||||
|
||||
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")
|
||||
raise PermissionError("You do not have permission to edit the public kiosk")
|
||||
return "public", "public"
|
||||
|
||||
if normalized_mode != "private":
|
||||
raise ValueError("Mode musi byc jednym z: public, private")
|
||||
raise ValueError("Mode must be one of: public, private")
|
||||
|
||||
if not auth_service.enabled:
|
||||
return "private", "private"
|
||||
|
||||
@@ -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"),
|
||||
}
|
||||
|
||||
@@ -108,6 +108,23 @@ class SQLiteAuthUserRepository:
|
||||
return None
|
||||
return self.get_by_username(username)
|
||||
|
||||
def update_role(self, username: str, role: str) -> AuthUser | None:
|
||||
now = datetime.utcnow().isoformat()
|
||||
with self.connect() as conn:
|
||||
cursor = conn.execute(
|
||||
"UPDATE auth_users SET role = ?, updated_at = ? WHERE username = ?",
|
||||
(role, now, username),
|
||||
)
|
||||
if cursor.rowcount == 0:
|
||||
return None
|
||||
return self.get_by_username(username)
|
||||
|
||||
def count_admin_users(self) -> int:
|
||||
with self.connect() as conn:
|
||||
row = conn.execute(
|
||||
"SELECT COUNT(*) AS count FROM auth_users WHERE role = 'admin' AND is_active = 1"
|
||||
).fetchone()
|
||||
return int(row['count']) if row is not None else 0
|
||||
|
||||
def list_users(self) -> list[AuthUser]:
|
||||
with self.connect() as conn:
|
||||
|
||||
@@ -7,11 +7,11 @@ from app.services.historical_sync import get_historical_sync_service
|
||||
from app.utils.serialization import to_plain
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser(description="Import dziennych agregatow PV z InfluxDB do lokalnego cache SQLite")
|
||||
parser.add_argument("--start-date", dest="start_date", help="Data startowa YYYY-MM-DD")
|
||||
parser.add_argument("--end-date", dest="end_date", help="Data koncowa YYYY-MM-DD")
|
||||
parser.add_argument("--chunk-days", dest="chunk_days", type=int, default=7, help="Liczba dni na chunk")
|
||||
parser.add_argument("--force", action="store_true", help="Nadpisz dni juz zapisane w cache")
|
||||
parser = argparse.ArgumentParser(description="Import daily PV aggregates from InfluxDB into the local SQLite cache")
|
||||
parser.add_argument("--start-date", dest="start_date", help="Start date YYYY-MM-DD")
|
||||
parser.add_argument("--end-date", dest="end_date", help="End date YYYY-MM-DD")
|
||||
parser.add_argument("--chunk-days", dest="chunk_days", type=int, default=7, help="Number of days per chunk")
|
||||
parser.add_argument("--force", action="store_true", help="Overwrite days already stored in the cache")
|
||||
args = parser.parse_args()
|
||||
|
||||
service = get_historical_sync_service()
|
||||
|
||||
@@ -59,7 +59,7 @@ APP_CONFIG = {
|
||||
}
|
||||
|
||||
SITE_CONFIG = {
|
||||
"site_name": os.getenv("SITE_NAME", "Domowa instalacja PV"),
|
||||
"site_name": os.getenv("SITE_NAME", "Home PV installation"),
|
||||
"timezone": APP_CONFIG["timezone"],
|
||||
"installed_power_kwp": env_float("PV_INSTALLED_POWER_KWP", 9.99),
|
||||
"currency": os.getenv("SITE_CURRENCY", "PLN"),
|
||||
@@ -91,20 +91,20 @@ CORS_ORIGINS = [
|
||||
]
|
||||
|
||||
TIME_RANGES = {
|
||||
"today": {"label": "Dzis", "special": "today"},
|
||||
"yesterday": {"label": "Wczoraj", "special": "yesterday"},
|
||||
"today": {"label": "Today", "special": "today"},
|
||||
"yesterday": {"label": "Yesterday", "special": "yesterday"},
|
||||
"6h": {"label": "6h", "seconds": 6 * 3600},
|
||||
"12h": {"label": "12h", "seconds": 12 * 3600},
|
||||
"24h": {"label": "24h", "seconds": 24 * 3600},
|
||||
"48h": {"label": "48h", "seconds": 48 * 3600},
|
||||
"1d": {"label": "1 dzien", "seconds": 1 * 24 * 3600},
|
||||
"3d": {"label": "3 dni", "seconds": 3 * 24 * 3600},
|
||||
"7d": {"label": "7 dni", "seconds": 7 * 24 * 3600},
|
||||
"14d": {"label": "14 dni", "seconds": 14 * 24 * 3600},
|
||||
"30d": {"label": "30 dni", "seconds": 30 * 24 * 3600},
|
||||
"60d": {"label": "60 dni", "seconds": 60 * 24 * 3600},
|
||||
"90d": {"label": "90 dni", "seconds": 90 * 24 * 3600},
|
||||
"365d": {"label": "365 dni", "seconds": 365 * 24 * 3600},
|
||||
"1d": {"label": "1 day", "seconds": 1 * 24 * 3600},
|
||||
"3d": {"label": "3 days", "seconds": 3 * 24 * 3600},
|
||||
"7d": {"label": "7 days", "seconds": 7 * 24 * 3600},
|
||||
"14d": {"label": "14 days", "seconds": 14 * 24 * 3600},
|
||||
"30d": {"label": "30 days", "seconds": 30 * 24 * 3600},
|
||||
"60d": {"label": "60 days", "seconds": 60 * 24 * 3600},
|
||||
"90d": {"label": "90 days", "seconds": 90 * 24 * 3600},
|
||||
"365d": {"label": "365 days", "seconds": 365 * 24 * 3600},
|
||||
"ytd": {"label": "YTD", "special": "ytd"},
|
||||
}
|
||||
|
||||
@@ -119,20 +119,20 @@ ANALYTICS = {
|
||||
"default_range": os.getenv("ANALYTICS_DEFAULT_RANGE", "30d"),
|
||||
"default_bucket": os.getenv("ANALYTICS_DEFAULT_BUCKET", "day"),
|
||||
"bucket_labels": {
|
||||
"day": "Dzien",
|
||||
"week": "Tydzien",
|
||||
"month": "Miesiac",
|
||||
"year": "Rok",
|
||||
"day": "Day",
|
||||
"week": "Week",
|
||||
"month": "Month",
|
||||
"year": "Year",
|
||||
},
|
||||
"compare_modes": {
|
||||
"none": "Porownanie",
|
||||
"previous_period": "Poprzedni okres",
|
||||
"previous_year": "Poprzedni rok",
|
||||
"previous_year_2": "2 lata wstecz",
|
||||
"previous_year_3": "3 lata wstecz",
|
||||
"previous_month_12": "12 miesiecy wstecz",
|
||||
"previous_month_24": "24 miesiace wstecz",
|
||||
"custom_multi": "Wlasne zakresy",
|
||||
"none": "Comparison",
|
||||
"previous_period": "Previous period",
|
||||
"previous_year": "Previous year",
|
||||
"previous_year_2": "2 years back",
|
||||
"previous_year_3": "3 years back",
|
||||
"previous_month_12": "12 months back",
|
||||
"previous_month_24": "24 months back",
|
||||
"custom_multi": "Custom ranges",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -208,7 +208,7 @@ register_metric(
|
||||
entity_id=os.getenv("PV_AC_POWER_ENTITY", "sofarsolar_ac_power"),
|
||||
measurement=os.getenv("PV_AC_POWER_MEASUREMENT", "W"),
|
||||
unit="W",
|
||||
label="Moc AC",
|
||||
label="AC Power",
|
||||
precision=0,
|
||||
)
|
||||
register_metric(
|
||||
@@ -216,7 +216,7 @@ register_metric(
|
||||
entity_id=os.getenv("PV_TOTAL_ENERGY_ENTITY", "sofarsolar_energy_total"),
|
||||
measurement=os.getenv("PV_TOTAL_ENERGY_MEASUREMENT", "kWh"),
|
||||
unit="kWh",
|
||||
label="Energia laczna",
|
||||
label="Total Energy",
|
||||
kind="counter",
|
||||
precision=2,
|
||||
)
|
||||
@@ -225,7 +225,7 @@ register_metric(
|
||||
entity_id=os.getenv("PV_INVERTER_TEMP_ENTITY", "sofarsolar_temprature_inverter"),
|
||||
measurement=os.getenv("PV_INVERTER_TEMP_MEASUREMENT", "°C"),
|
||||
unit="°C",
|
||||
label="Temperatura falownika",
|
||||
label="Inverter Temperature",
|
||||
precision=1,
|
||||
)
|
||||
|
||||
@@ -243,7 +243,7 @@ for index, defaults in STRING_DEFAULTS.items():
|
||||
entity_id=os.getenv(f"PV_STRING_{index}_POWER_ENTITY", defaults["power"]),
|
||||
measurement=os.getenv(f"PV_STRING_{index}_POWER_MEASUREMENT", "W"),
|
||||
unit="W",
|
||||
label=f"{label} moc",
|
||||
label=f"{label} power",
|
||||
precision=0,
|
||||
)
|
||||
voltage_metric_id = register_metric(
|
||||
@@ -251,7 +251,7 @@ for index, defaults in STRING_DEFAULTS.items():
|
||||
entity_id=os.getenv(f"PV_STRING_{index}_VOLTAGE_ENTITY", defaults["voltage"]),
|
||||
measurement=os.getenv(f"PV_STRING_{index}_VOLTAGE_MEASUREMENT", "V"),
|
||||
unit="V",
|
||||
label=f"{label} napiecie",
|
||||
label=f"{label} voltage",
|
||||
precision=1,
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user