first commit

This commit is contained in:
Mateusz Gruszczyński
2026-03-23 15:56:18 +01:00
commit c5cc2efbac
106 changed files with 10254 additions and 0 deletions

View File

@@ -0,0 +1,15 @@
from .analytics import analytics_blueprint
from .auth import auth_blueprint
from .dashboard import dashboard_blueprint
from .health import health_blueprint
from .historical import historical_blueprint
from .realtime import realtime_blueprint
__all__ = [
"auth_blueprint",
"analytics_blueprint",
"dashboard_blueprint",
"health_blueprint",
"historical_blueprint",
"realtime_blueprint",
]

View File

@@ -0,0 +1,63 @@
from __future__ import annotations
import json
from flask import Blueprint, jsonify, request
from app.services.analytics import AnalyticsService
from app.utils.serialization import to_plain
analytics_blueprint = Blueprint("analytics", __name__)
service = AnalyticsService()
@analytics_blueprint.get("/analytics/production")
def production_analytics():
range_key = request.args.get("range", "30d")
bucket = request.args.get("bucket", "day")
compare = request.args.get("compare", "none")
start = request.args.get("start")
end = request.args.get("end")
compare_ranges_raw = request.args.get("compare_ranges", "")
compare_ranges = []
if compare_ranges_raw:
try:
compare_ranges = json.loads(compare_ranges_raw)
except json.JSONDecodeError as exc:
return jsonify({"detail": f"Invalid compare_ranges payload: {exc}"}), 400
try:
return jsonify(
to_plain(
service.production(
range_key=range_key,
bucket=bucket,
compare_mode=compare,
start=start,
end=end,
compare_ranges=compare_ranges,
)
)
)
except ValueError as exc:
return jsonify({"detail": str(exc)}), 400
@analytics_blueprint.get("/analytics/distribution")
def production_distribution():
range_key = request.args.get("range", "30d")
bucket = request.args.get("bucket", "day")
start = request.args.get("start")
end = request.args.get("end")
try:
return jsonify(
to_plain(
service.distribution(
range_key=range_key,
bucket=bucket,
start=start,
end=end,
)
)
)
except ValueError as exc:
return jsonify({"detail": str(exc)}), 400

View File

@@ -0,0 +1,80 @@
from __future__ import annotations
from flask import Blueprint, jsonify, request
from app.services.auth import get_auth_service
from app.utils.serialization import to_plain
auth_blueprint = Blueprint("auth", __name__)
service = get_auth_service()
@auth_blueprint.get("/auth/status")
def auth_status():
return jsonify(to_plain(service.status()))
@auth_blueprint.post("/auth/login")
def auth_login():
payload = request.get_json(silent=True) or {}
try:
status = service.login(payload.get("username", ""), payload.get("password", ""))
return jsonify(to_plain(status))
except ValueError as exc:
return jsonify({"detail": str(exc)}), 401
@auth_blueprint.post("/auth/logout")
def auth_logout():
return jsonify(to_plain(service.logout()))
@auth_blueprint.get("/auth/users")
def list_users():
try:
service.require_admin()
return jsonify(to_plain({"items": service.list_users()}))
except PermissionError as exc:
return jsonify({"detail": str(exc)}), 403
@auth_blueprint.post("/auth/users")
def create_user():
payload = request.get_json(silent=True) or {}
try:
service.require_admin()
user = service.create_user(
username=payload.get("username", ""),
password=payload.get("password", ""),
role=payload.get("role", "user"),
display_name=payload.get("display_name") or payload.get("username") or "",
)
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
@auth_blueprint.post("/auth/users/<username>/reset-password")
def reset_password(username: str):
payload = request.get_json(silent=True) or {}
try:
service.require_admin()
user = service.reset_password(username=username, new_password=payload.get("password", ""))
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

View File

@@ -0,0 +1,80 @@
from __future__ import annotations
from flask import Blueprint, jsonify, request
from app.core_settings import get_settings
from app.services.capabilities import build_capabilities
from app.services.catalog import get_catalog
from app.services.kiosk_settings import get_kiosk_settings_service
from app.services.auth import get_auth_service
from app.utils.serialization import to_plain
dashboard_blueprint = Blueprint("dashboard", __name__)
@dashboard_blueprint.get("/dashboard/config")
def dashboard_config():
settings = get_settings()
catalog = get_catalog()
capabilities = build_capabilities(catalog)
payload = {
"app": {
"name": settings.app_name,
"version": settings.version,
"site_name": settings.site_name,
"timezone": settings.timezone,
"installed_power_kwp": settings.installed_power_kwp,
},
"defaults": {
"realtime_range": settings.realtime["history_default_range"],
"analytics_range": settings.analytics["default_range"],
"analytics_bucket": settings.analytics["default_bucket"],
"tab": settings.frontend_defaults["tab"],
"theme": settings.frontend_defaults["theme"],
"language": settings.frontend_defaults["language"],
},
"auth": {
"enabled": settings.auth["enabled"],
},
"i18n": settings.i18n,
"capabilities": capabilities,
"visible_entities": [
{
"metric_id": metric.id,
"label": metric.label,
"entity_id": metric.entity_id,
"measurement": metric.measurement,
"unit": metric.unit,
"kind": metric.kind,
}
for metric in catalog.visible_entities()
],
}
return jsonify(to_plain(payload))
@dashboard_blueprint.get("/dashboard/kiosk-settings")
def dashboard_kiosk_settings():
requested_mode = request.args.get("mode") or ("public" if request.args.get("publicKiosk") == "1" else "private")
try:
payload = get_kiosk_settings_service().get(requested_mode)
return jsonify(to_plain(payload))
except ValueError as exc:
return jsonify({"detail": str(exc)}), 400
@dashboard_blueprint.put("/dashboard/kiosk-settings")
def update_dashboard_kiosk_settings():
payload = request.get_json(silent=True) or {}
mode = payload.get("mode", "private")
auth_service = get_auth_service()
try:
auth_service.require_admin()
updated = get_kiosk_settings_service().update_from_session(mode, payload)
return jsonify(to_plain(updated))
except PermissionError as exc:
return jsonify({"detail": str(exc)}), 403
except ValueError as exc:
return jsonify({"detail": str(exc)}), 400

View File

@@ -0,0 +1,17 @@
from __future__ import annotations
from flask import Blueprint, jsonify
from app.core_settings import get_settings
health_blueprint = Blueprint("health", __name__)
@health_blueprint.get("/health")
def health():
settings = get_settings()
return jsonify({
"status": "ok",
"app": settings.app_name,
"version": settings.version,
})

View File

@@ -0,0 +1,54 @@
from __future__ import annotations
from datetime import date
from flask import Blueprint, jsonify, request
from app.services.historical_sync import get_historical_sync_service
from app.utils.serialization import to_plain
historical_blueprint = Blueprint("historical", __name__)
service = get_historical_sync_service()
@historical_blueprint.get("/historical/status")
def historical_status():
return jsonify(to_plain(service.status()))
@historical_blueprint.post("/historical/start")
def historical_start():
payload = request.get_json(silent=True) or {}
try:
status = service.start(
start_date=_parse_date(payload.get("start_date")),
end_date=_parse_date(payload.get("end_date")),
chunk_days=payload.get("chunk_days"),
force=bool(payload.get("force", False)),
)
return jsonify(to_plain(status))
except ValueError as exc:
return jsonify({"detail": str(exc)}), 400
except RuntimeError as exc:
return jsonify({"detail": str(exc)}), 400
@historical_blueprint.post("/historical/sync-now")
def historical_sync_now():
try:
status = service.start(auto=True)
return jsonify(to_plain(status))
except RuntimeError as exc:
return jsonify({"detail": str(exc)}), 400
@historical_blueprint.post("/historical/cancel")
def historical_cancel():
return jsonify(to_plain(service.cancel()))
def _parse_date(value: str | None) -> date | None:
if not value:
return None
return date.fromisoformat(value)

View File

@@ -0,0 +1,26 @@
from __future__ import annotations
from flask import Blueprint, jsonify, request
from app.services.realtime import RealtimeService
from app.utils.serialization import to_plain
realtime_blueprint = Blueprint("realtime", __name__)
service = RealtimeService()
@realtime_blueprint.get("/realtime/snapshot")
def realtime_snapshot():
return jsonify(to_plain(service.snapshot()))
@realtime_blueprint.get("/realtime/history")
def realtime_history():
range_key = request.args.get("range", "6h")
start = request.args.get("start")
end = request.args.get("end")
metrics = [item.strip() for item in request.args.get("metrics", "").split(",") if item.strip()]
try:
return jsonify(to_plain(service.history(range_key=range_key, start=start, end=end, metric_ids=metrics or None)))
except ValueError as exc:
return jsonify({"detail": str(exc)}), 400