47c65268e3
The per-unit Gantt chart on /unit/{id} (Phase 1, v0.11.0) was scoped
to one unit's deployment timeline. This adds the fleet-wide view as
a new entry under /tools.
What it shows
- 12-month calendar grid styled like the Job Planner (4 months per
row, responsive down to single column on mobile).
- Each day cell shows up to 4 colored mini-bars — one per project
that had ≥1 active UnitAssignment that day, color deterministically
hashed from project_id. Days with >4 active projects show "+N".
- KPI strip at the top: project count, distinct unit count, total
assignment count in the window.
- Collapsible project legend: ordered by first-active date (which
matches the deployment-history reading order), each row links to
the project page, shows the assignment count.
Click-a-day side panel
- Click any populated day cell → slide-over panel from the right
- Groups by project, lists every (unit, location) active that day
- Per-deployment: unit link, location link, window dates, active /
closed badge, "auto-backfilled" tag for metadata_backfill source
- Sources from a new GET /api/admin/deployment-history/day endpoint
Navigation
- Prev / Next month buttons shift the 12-month window by one month
- "Recent" button jumps back to default (12 months ending now)
- Default window is 11 months back from current month — operator
sees the recent past on first load, not future emptiness
Files
- backend/services/deployment_history.py — data builder + day-detail
helper. Walks UnitAssignment windows, intersects with the 12-month
range, computes per-project active-day sets.
- backend/routers/deployment_history.py — page route + day-detail JSON
endpoint. Wired into main.py.
- templates/admin/deployment_history.html — page + side-panel
- templates/tools.html — new card linking to the page
Phase 3 (deferred): drag-to-resize bars to retroactively adjust
assignment windows from inside the calendar; per-unit row view
(complement to the project-row view) for "where has unit X been across
all jobs"; horizontal scroll for >12-month windows.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
100 lines
3.2 KiB
Python
100 lines
3.2 KiB
Python
"""
|
|
Fleet-wide deployment-history calendar — Phase 2 of the
|
|
deployment-history visualisation work (Phase 1 is the per-unit Gantt
|
|
on /unit/{id}).
|
|
|
|
Renders all UnitAssignment windows across all projects on a 12-month
|
|
calendar grid styled like the Job Planner. Each day cell shows one
|
|
mini-bar per project that had ≥1 active assignment that day. Click a
|
|
day → side panel with the (unit, location) pairs active.
|
|
|
|
Routes:
|
|
GET /tools/deployment-history — HTML page
|
|
GET /api/admin/deployment-history/day — JSON list of deployments
|
|
on a specific date (used
|
|
by the day-detail panel)
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import date, datetime
|
|
from typing import Optional
|
|
|
|
from fastapi import APIRouter, Depends, Query, Request
|
|
from fastapi.responses import HTMLResponse, JSONResponse
|
|
from sqlalchemy.orm import Session
|
|
|
|
from backend.database import get_db
|
|
from backend.services.deployment_history import (
|
|
get_deployment_history_data,
|
|
get_deployments_on_day,
|
|
)
|
|
from backend.templates_config import templates
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.get("/tools/deployment-history", response_class=HTMLResponse)
|
|
def deployment_history_page(
|
|
request: Request,
|
|
year: Optional[int] = Query(None),
|
|
month: Optional[int] = Query(None),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""Fleet-wide deployment history calendar.
|
|
|
|
Defaults to a 12-month window ending in the current month (so the
|
|
operator sees the recent past, not the future). ?year=&month= can
|
|
override the START of the window to scroll backward or forward.
|
|
"""
|
|
today = date.today()
|
|
# Default: 12-month window ending this month → start = 11 months back.
|
|
if year is None or month is None:
|
|
# 11 months back from current month.
|
|
m = today.month - 11
|
|
y = today.year
|
|
while m < 1:
|
|
m += 12
|
|
y -= 1
|
|
start_year, start_month = y, m
|
|
else:
|
|
start_year, start_month = year, month
|
|
|
|
calendar = get_deployment_history_data(db, start_year, start_month)
|
|
|
|
# Build prev/next navigation values.
|
|
prev_y, prev_m = (start_year - 1, 12) if start_month == 1 else (start_year, start_month - 1)
|
|
next_y, next_m = (start_year + 1, 1) if start_month == 12 else (start_year, start_month + 1)
|
|
|
|
return templates.TemplateResponse("admin/deployment_history.html", {
|
|
"request": request,
|
|
"calendar": calendar,
|
|
"today": today.isoformat(),
|
|
"prev_year": prev_y,
|
|
"prev_month": prev_m,
|
|
"next_year": next_y,
|
|
"next_month": next_m,
|
|
})
|
|
|
|
|
|
@router.get("/api/admin/deployment-history/day")
|
|
def deployment_history_day(
|
|
target_date: str = Query(..., description="YYYY-MM-DD"),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""Return assignments active on a specific calendar day."""
|
|
try:
|
|
d = date.fromisoformat(target_date)
|
|
except ValueError:
|
|
return JSONResponse(
|
|
{"error": f"Invalid date: {target_date!r}"},
|
|
status_code=400,
|
|
)
|
|
|
|
deployments = get_deployments_on_day(db, d)
|
|
return JSONResponse({
|
|
"date": target_date,
|
|
"count": len(deployments),
|
|
"deployments": deployments,
|
|
})
|