feat: add report_date to monitoring sessions and update related functionality
fix: chart properly renders centered
This commit is contained in:
41
backend/migrate_add_session_report_date.py
Normal file
41
backend/migrate_add_session_report_date.py
Normal file
@@ -0,0 +1,41 @@
|
||||
"""
|
||||
Migration: add report_date to monitoring_sessions.
|
||||
|
||||
Run once:
|
||||
python backend/migrate_add_session_report_date.py
|
||||
|
||||
Or inside the container:
|
||||
docker exec terra-view-terra-view-1 python3 backend/migrate_add_session_report_date.py
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from backend.database import engine
|
||||
from sqlalchemy import text
|
||||
|
||||
def run():
|
||||
with engine.connect() as conn:
|
||||
# Check which columns already exist
|
||||
result = conn.execute(text("PRAGMA table_info(monitoring_sessions)"))
|
||||
existing = {row[1] for row in result}
|
||||
|
||||
added = []
|
||||
for col, definition in [
|
||||
("report_date", "DATE"),
|
||||
]:
|
||||
if col not in existing:
|
||||
conn.execute(text(f"ALTER TABLE monitoring_sessions ADD COLUMN {col} {definition}"))
|
||||
added.append(col)
|
||||
else:
|
||||
print(f" Column '{col}' already exists — skipping.")
|
||||
|
||||
conn.commit()
|
||||
|
||||
if added:
|
||||
print(f" Added columns: {', '.join(added)}")
|
||||
print("Migration complete.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
@@ -309,6 +309,11 @@ class MonitoringSession(Base):
|
||||
period_start_hour = Column(Integer, nullable=True)
|
||||
period_end_hour = Column(Integer, nullable=True)
|
||||
|
||||
# For day sessions: the specific calendar date to use for report filtering.
|
||||
# Overrides the automatic "last date with daytime rows" heuristic.
|
||||
# Null = use heuristic.
|
||||
report_date = Column(Date, nullable=True)
|
||||
|
||||
# Snapshot of device configuration at recording time
|
||||
session_metadata = Column(Text, nullable=True) # JSON
|
||||
|
||||
|
||||
@@ -1159,7 +1159,7 @@ async def get_project_sessions(
|
||||
if session.period_start_hour is not None and session.period_end_hour is not None and session.started_at:
|
||||
from datetime import date as _date
|
||||
local_start = utc_to_local(session.started_at)
|
||||
start_day = local_start.date()
|
||||
start_day = session.report_date if session.report_date else local_start.date()
|
||||
sh = session.period_start_hour
|
||||
eh = session.period_end_hour
|
||||
|
||||
@@ -1259,10 +1259,11 @@ async def get_sessions_calendar(
|
||||
# Build calendar grid (Mon–Sun weeks)
|
||||
first_day = _date(year, month, 1)
|
||||
last_day = _date(year, month, monthrange(year, month)[1])
|
||||
# Start on Monday before first_day
|
||||
grid_start = first_day - _td(days=first_day.weekday())
|
||||
# End on Sunday after last_day
|
||||
days_after = 6 - last_day.weekday()
|
||||
# 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)
|
||||
|
||||
weeks = []
|
||||
@@ -2061,6 +2062,17 @@ async def patch_session(
|
||||
except (ValueError, TypeError):
|
||||
raise HTTPException(status_code=400, detail=f"{field} must be an integer 0–23 or null")
|
||||
|
||||
if "report_date" in data:
|
||||
val = data["report_date"]
|
||||
if val is None or val == "":
|
||||
session.report_date = None
|
||||
else:
|
||||
try:
|
||||
from datetime import date as _date
|
||||
session.report_date = _date.fromisoformat(str(val))
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Invalid report_date format. Use YYYY-MM-DD.")
|
||||
|
||||
db.commit()
|
||||
return JSONResponse({
|
||||
"status": "success",
|
||||
@@ -2068,6 +2080,7 @@ async def patch_session(
|
||||
"period_type": session.period_type,
|
||||
"period_start_hour": session.period_start_hour,
|
||||
"period_end_hour": session.period_end_hour,
|
||||
"report_date": session.report_date.isoformat() if session.report_date else None,
|
||||
})
|
||||
|
||||
|
||||
@@ -2103,7 +2116,7 @@ async def view_session_detail(
|
||||
effective_range = None
|
||||
if session.period_start_hour is not None and session.period_end_hour is not None and session.started_at:
|
||||
local_start = utc_to_local(session.started_at)
|
||||
start_day = local_start.date()
|
||||
start_day = session.report_date if session.report_date else local_start.date()
|
||||
sh = session.period_start_hour
|
||||
eh = session.period_end_hour
|
||||
def _fmt_h(h):
|
||||
@@ -2137,6 +2150,7 @@ async def view_session_detail(
|
||||
"files": files,
|
||||
"effective_range": effective_range,
|
||||
"session_meta": session_meta,
|
||||
"report_date": session.report_date.isoformat() if session.report_date else "",
|
||||
})
|
||||
|
||||
|
||||
@@ -2672,7 +2686,7 @@ async def generate_excel_report(
|
||||
_plot_border.ln.solidFill = "000000"
|
||||
_plot_border.ln.w = 12700
|
||||
chart.plot_area.spPr = _plot_border
|
||||
ws.add_chart(chart, "H4")
|
||||
ws.add_chart(chart, "I4")
|
||||
|
||||
# --- Stats table: note at I28-I29, headers at I31, data rows 32-34 ---
|
||||
note1 = ws.cell(row=28, column=9, value="Note: Averages are calculated by determining the arithmetic average ")
|
||||
@@ -3160,7 +3174,7 @@ async def generate_report_from_preview(
|
||||
_plot_border.ln.solidFill = "000000"
|
||||
_plot_border.ln.w = 12700
|
||||
chart.plot_area.spPr = _plot_border
|
||||
ws.add_chart(chart, "H4")
|
||||
ws.add_chart(chart, "I4")
|
||||
|
||||
# --- Stats block starting at I28 ---
|
||||
# Stats table: note at I28-I29, headers at I31, data rows 32-34, border row 35
|
||||
@@ -3508,7 +3522,7 @@ async def generate_combined_excel_report(
|
||||
_plot_border.ln.solidFill = "000000"
|
||||
_plot_border.ln.w = 12700
|
||||
chart.plot_area.spPr = _plot_border
|
||||
ws.add_chart(chart, "H4")
|
||||
ws.add_chart(chart, "I4")
|
||||
|
||||
# Stats table: note at I28-I29, headers at I31, data rows 32-34, border row 35
|
||||
note1 = ws.cell(row=28, column=9, value="Note: Averages are calculated by determining the arithmetic average ")
|
||||
@@ -3751,6 +3765,7 @@ def _build_location_data_from_sessions(project_id: str, db, selected_session_ids
|
||||
"period_start_hour": session.period_start_hour,
|
||||
"period_end_hour": session.period_end_hour,
|
||||
"started_at": session.started_at,
|
||||
"report_date": session.report_date,
|
||||
"rows": [],
|
||||
}
|
||||
|
||||
@@ -3807,10 +3822,13 @@ def _build_location_data_from_sessions(project_id: str, db, selected_session_ids
|
||||
if is_day_session:
|
||||
# Day-style: start_h <= hour < end_h, restricted to the LAST calendar date
|
||||
in_window = lambda h: sh <= h < eh
|
||||
daytime_dates = sorted({
|
||||
dt.date() for dt, row in parsed if dt and in_window(dt.hour)
|
||||
})
|
||||
target_date = daytime_dates[-1] if daytime_dates else None
|
||||
if entry.get("report_date"):
|
||||
target_date = entry["report_date"]
|
||||
else:
|
||||
daytime_dates = sorted({
|
||||
dt.date() for dt, row in parsed if dt and in_window(dt.hour)
|
||||
})
|
||||
target_date = daytime_dates[-1] if daytime_dates else None
|
||||
filtered = [
|
||||
(dt, row) for dt, row in parsed
|
||||
if dt and dt.date() == target_date and in_window(dt.hour)
|
||||
@@ -4101,7 +4119,7 @@ async def generate_combined_from_preview(
|
||||
_plot_border.ln.solidFill = "000000"
|
||||
_plot_border.ln.w = 12700
|
||||
chart.plot_area.spPr = _plot_border
|
||||
ws.add_chart(chart, "H4")
|
||||
ws.add_chart(chart, "I4")
|
||||
|
||||
hdr_fill_tbl = PatternFill(start_color="F2F2F2", end_color="F2F2F2", fill_type="solid")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user