calendar
This commit is contained in:
+179
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user