This commit is contained in:
2026-11-03 08:44:11 +01:00
parent cbf5f20c75
commit 0a9da4f9c5
13 changed files with 422 additions and 5 deletions
+179
View File
@@ -2,7 +2,9 @@ import fcntl
import os
import uuid
from contextlib import contextmanager
from datetime import date, datetime, timedelta, timezone
from pathlib import Path
from zoneinfo import ZoneInfo
import click
from flask import (
@@ -49,6 +51,34 @@ def format_bytes(value: int) -> str:
return f"{int(size)} B"
LOCAL_TZ_DEFAULT = "Europe/Warsaw"
def _calendar_tz(name):
try:
return ZoneInfo(name)
except Exception:
return ZoneInfo(LOCAL_TZ_DEFAULT)
def _parse_date_arg(raw):
value = (raw or "").strip()
if not value:
return None
try:
return date.fromisoformat(value)
except ValueError:
return None
def _day_bounds_utc(day: date, tz):
start_local = datetime(day.year, day.month, day.day)
end_local = start_local + timedelta(days=1)
start_utc = start_local.replace(tzinfo=tz).astimezone(timezone.utc).replace(tzinfo=None)
end_utc = end_local.replace(tzinfo=tz).astimezone(timezone.utc).replace(tzinfo=None)
return start_utc, end_utc
def create_app():
app = Flask(__name__)
@@ -326,6 +356,155 @@ def create_app():
flash(f"Deleted {name}.", "success")
return redirect(url_for("clipboard_page"))
@app.route("/api/sensors/<int:sensor_id>/readouts/range")
@login_required
def api_sensor_readouts_range(sensor_id):
return _proxy(
lambda: sensors.sensor_readouts_range(sensor_id, request.args.to_dict())
)
@app.route("/calendar")
@login_required
def calendar_page():
view = (request.args.get("view") or "month").strip().lower()
if view not in ("month", "week"):
view = "month"
tz = _calendar_tz(request.args.get("tz") or LOCAL_TZ_DEFAULT)
now_local = datetime.now(tz)
year = now_local.year
month = now_local.month
week_start = None
week_end = None
week_prev = None
week_next = None
days = []
month_weeks = None
if view == "week":
week_date = _parse_date_arg(request.args.get("date")) or now_local.date()
week_start = week_date - timedelta(days=week_date.weekday())
week_end = week_start + timedelta(days=6)
week_prev = week_start - timedelta(days=7)
week_next = week_start + timedelta(days=7)
day_list = [week_start + timedelta(days=i) for i in range(7)]
range_from = _day_bounds_utc(week_start, tz)[0]
range_to = _day_bounds_utc(week_end + timedelta(days=1), tz)[0]
else:
year = int(request.args.get("year") or now_local.year)
month = int(request.args.get("month") or now_local.month)
if not 1 <= month <= 12:
month = 1
first_day = date(year, month, 1)
next_year = year + 1 if month == 12 else year
next_month = 1 if month == 12 else month + 1
last_day = date(next_year, next_month, 1) - timedelta(days=1)
day_list = []
current = first_day
while current <= last_day:
day_list.append(current)
current += timedelta(days=1)
range_from, _ = _day_bounds_utc(first_day, tz)
range_to, _ = _day_bounds_utc(last_day, tz)
week_start = first_day - timedelta(days=first_day.weekday())
week_end = last_day
readouts_by_day = {day: [] for day in day_list}
sensor_names = {}
try:
listed = sensors.list_sensors()
for sensor in listed[1] or []:
sensor_id = int(sensor["id"])
sensor_names[sensor_id] = sensor["name"]
status, payload = sensors.sensor_readouts_range(
sensor_id,
{"from": range_from.isoformat(), "to": range_to.isoformat()},
)
if status != 200:
continue
for item in payload or []:
raw_ts = (item.get("timestamp") or "").rstrip("Z")
ts = datetime.fromisoformat(raw_ts) if raw_ts else None
if ts is None:
continue
local_day = ts.replace(tzinfo=timezone.utc).astimezone(tz).date()
if local_day in readouts_by_day:
readouts_by_day[local_day].append(
{
"id": item.get("id"),
"sensor_id": item.get("sensor_id") or sensor_id,
"type": item.get("type"),
"value": item.get("value"),
"timestamp": ts,
}
)
except SensorsAPIError:
pass
days = [
{"date": day, "readouts": readouts_by_day.get(day, [])}
for day in day_list
]
month_weeks = None
if view == "month":
first_monday = week_start
last_sunday = week_end + timedelta(days=6 - week_end.weekday())
end_exclusive = last_sunday + timedelta(days=1)
weeks = []
week = []
current = first_monday
while current < end_exclusive:
week.append(
{
"date": current,
"in_month": current.month == month,
"readouts": readouts_by_day.get(current, []),
}
)
if current.weekday() == 6:
weeks.append(week)
week = []
current += timedelta(days=1)
if week:
weeks.append(week)
month_weeks = weeks
if month == 1:
prev_year, prev_month = year - 1, 12
else:
prev_year, prev_month = year, month - 1
if month == 12:
next_year, next_month = year + 1, 1
else:
next_year, next_month = year, month + 1
month_names = (
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December",
)
month_label = f"{month_names[month - 1]} {year}"
return render_template(
"calendar.html",
user=current_user,
view=view,
year=year,
month=month,
week_start=week_start,
week_end=week_end,
days=days,
month_weeks=month_weeks,
sensor_names=sensor_names,
prev_year=prev_year,
prev_month=prev_month,
next_year=next_year,
next_month=next_month,
week_prev=week_prev,
week_next=week_next,
today=now_local.date(),
)
# --- Notifications (stored in main_app.db) ---
@app.route("/notifications")