Timelines (The Train Timetable With Ghost Trains)
The Departures Board
At a railway station there is a big timetable of the day. Trains that have already gone show when they really left and arrived, and a late one is longer than planned. Trains still to come are drawn as pale ghost trains at the time the timetable promises. A thin line marks "now".
PyScheduler's /timeline is that board. Each job is a lane (a track).
Each attempt is a coloured bar (green succeeded, red failed, blue
running). A run's retries sit side by side. A bar that is still running
stretches to the "now" line. The forecast adds ghost bars (dashed
outline) for the part of the window that is still in the future.
Zoom
| Zoom | The window |
|---|---|
hour |
the last hour and the next hour, centred on now |
day |
midnight to midnight, local time, on the date asked for (the default is today) |
week |
seven local days from the date asked for |
A week is the longest window. Lanes only exist for jobs that have an attempt or a forecast entry in the window, sorted by name; the axis has ticks every 10 minutes, every hour or every day. Previous and next links step by a day (or a week); they are left out at the ends of the calendar the pages can draw (1900-01-01 to 2999-12-31).
The Day the Clocks Change
A "day" is midnight to midnight on the wall clock, so on the day
clocks go back the window is 25 hours long, and on the day they go
forward it is 23. Positions are worked out from the real elapsed time,
so bars do not drift. A run's date is not always the day it runs: a
previous_business_day job on a Monday works for Friday, a backfill
runs today for old dates, and a job in a zone ahead of the scheduler's
can run "tomorrow" this evening. So the page reads the runs whose run
date is up to a week before or a day after the shown days, and then
keeps only the attempts that really overlap the window.
Busy Lanes and Odd Attempts
A job due every minute would have about 10,000 ghost bars in a week. A
lane draws at most 200 (GHOST_CAP); after that one merged ghost band
covers the rest of the expected runs, with a tip saying how many. An
attempt with no end and no outcome whose run is not running (it was
lost before it could say how it ended) is drawn grey, as unknown.
Try It
from datetime import UTC, date, datetime
from zoneinfo import ZoneInfo
from pyscheduler.ui.timeline_window import Zoom, window_for
london = ZoneInfo("Europe/London")
now = datetime(2026, 10, 25, 12, 0, tzinfo=UTC) # the day the clocks go back
day = window_for(Zoom.DAY, date(2026, 10, 25), now, london)
print(day.span) # 1 day, 1:00:00 (25 hours)
hour = window_for(Zoom.HOUR, date(2026, 10, 25), now, london)
print(hour.span, hour.fraction(now)) # 2:00:00 0.5
week = window_for(Zoom.WEEK, date(2026, 10, 25), now, london)
print(week.span) # 7 days, 1:00:00