fix: adds timeline bars to SLM calendar view, more conscise and legible.

This commit is contained in:
2026-03-27 22:44:53 +00:00
parent 192e15f238
commit 27eeb0fae6
2 changed files with 145 additions and 55 deletions

View File

@@ -1235,36 +1235,83 @@ async def get_sessions_calendar(
if loc:
loc_names[lid] = loc.name
# Build day -> list of session dots
# day key: date object
# Build calendar grid bounds first (needed for session spanning logic)
first_day = _date(year, month, 1)
last_day = _date(year, month, monthrange(year, month)[1])
days_before = (first_day.isoweekday() % 7)
grid_start = first_day - _td(days=days_before)
days_after = 6 - (last_day.isoweekday() % 7)
grid_end = last_day + _td(days=days_after)
def _period_hours(s):
"""Return (start_hour, end_hour) for a session, falling back to period_type defaults."""
psh, peh = s.period_start_hour, s.period_end_hour
if psh is None or peh is None:
if s.period_type and "night" in s.period_type:
return 19, 7
if s.period_type and "day" in s.period_type:
return 7, 19
return psh, peh
# Build day -> list of gantt segments
day_sessions: dict = {}
for s in sessions:
if not s.started_at:
continue
local_start = utc_to_local(s.started_at)
d = local_start.date()
if d.year == year and d.month == month:
if d not in day_sessions:
day_sessions[d] = []
day_sessions[d].append({
"session_id": s.id,
"label": s.session_label or f"Session {s.id[:8]}",
"location_id": s.location_id,
"location_name": loc_names.get(s.location_id, "Unknown"),
"color": loc_color.get(s.location_id, "#9ca3af"),
"status": s.status,
"period_type": s.period_type,
})
local_end = utc_to_local(s.stopped_at) if s.stopped_at else now_local
span_start = local_start.date()
span_end = local_end.date()
psh, peh = _period_hours(s)
# Build calendar grid (MonSun weeks)
first_day = _date(year, month, 1)
last_day = _date(year, month, monthrange(year, month)[1])
# Start on Sunday before first_day (isoweekday: Mon=1 … Sun=7; weekday: Mon=0 … Sun=6)
days_before = (first_day.isoweekday() % 7) # Sun=0, Mon=1, …, Sat=6
grid_start = first_day - _td(days=days_before)
# End on Saturday after last_day
days_after = 6 - (last_day.isoweekday() % 7)
grid_end = last_day + _td(days=days_after)
cur_d = span_start
while cur_d <= span_end:
if grid_start <= cur_d <= grid_end:
# Device bar bounds (hours 024 within this day)
dev_sh = (local_start.hour + local_start.minute / 60.0) if cur_d == span_start else 0.0
dev_eh = (local_end.hour + local_end.minute / 60.0) if cur_d == span_end else 24.0
# Effective window within this day
eff_sh = eff_eh = None
if psh is not None and peh is not None:
if psh < peh:
# Day window e.g. 7→19
eff_sh, eff_eh = float(psh), float(peh)
else:
# Night window crossing midnight e.g. 19→7
if cur_d == span_start:
eff_sh, eff_eh = float(psh), 24.0
else:
eff_sh, eff_eh = 0.0, float(peh)
# Format tooltip labels
def _fmt_h(h):
hh = int(h) % 24
mm = int((h % 1) * 60)
suffix = "AM" if hh < 12 else "PM"
return f"{hh % 12 or 12}:{mm:02d} {suffix}"
if cur_d not in day_sessions:
day_sessions[cur_d] = []
day_sessions[cur_d].append({
"session_id": s.id,
"label": s.session_label or f"Session {s.id[:8]}",
"location_id": s.location_id,
"location_name": loc_names.get(s.location_id, "Unknown"),
"color": loc_color.get(s.location_id, "#9ca3af"),
"status": s.status,
"period_type": s.period_type,
# Gantt bar percentages (0100 scale across 24 hours)
"dev_start_pct": round(dev_sh / 24 * 100, 1),
"dev_width_pct": max(1.5, round((dev_eh - dev_sh) / 24 * 100, 1)),
"eff_start_pct": round(eff_sh / 24 * 100, 1) if eff_sh is not None else None,
"eff_width_pct": max(1.0, round((eff_eh - eff_sh) / 24 * 100, 1)) if eff_sh is not None else None,
"dev_start_label": _fmt_h(dev_sh),
"dev_end_label": _fmt_h(dev_eh),
"eff_start_label": f"{int(psh):02d}:00" if eff_sh is not None else None,
"eff_end_label": f"{int(peh):02d}:00" if eff_sh is not None else None,
})
cur_d += _td(days=1)
weeks = []
cur = grid_start