# Timeline Page Review and Refactor Plan Date: 2026-05-31 ## Context This project is a Flask/Jinja/SQLAlchemy KSP operations admin app. The original data center still covers engines, communications, tanks, vehicle costs, fuel conversion, workbook import, and Wiki publishing helpers. The recent refactor described in `draft_board.md` expands the app into mission operations: - `Asset`, `AssetLogEntry`, and `AssetStateNode` are now the core timeline data model. - Current state, current location, current mission, upcoming events, status board, location board, asset catalog, and timeline are derived from logs at `simulation_time`. - Logs are split into `state` intervals and `event` points; state intervals may contain timed state nodes. - Location is intended to move from free text toward a structured hierarchy. - Timeline is intended to be a read-only navigable view with wheel zoom, drag pan, type/location/state/date filters, and a filtered asset selector. Current timeline implementation lives mainly in: - `app/routes/web.py`: `/mission-preview/timeline`, scale/range helpers, row/segment/event building. - `app/templates/mission_timeline_preview.html`: full page, filter bar, wheel zoom script, tooltip script. - `app/templates/mission_timeline_partial.html`: AJAX replacement fragment. - `app/static/styles.css`: legacy `.tl-*` styles plus shared design system. ## Current Timeline Flow 1. The route loads every asset with logs through `_load_assets_with_logs()`. 2. It builds snapshot rows through `_build_mission_board_rows()`. 3. It filters rows by search, `asset_type`, `location`, `record_scope`, and optional `asset_ids`. 4. It resolves the visible time range and scale. 5. `_build_timeline_rows()` converts state intervals into segments and event/state nodes into event markers. 6. The template renders one sticky asset-name column and one scrollable absolute-position lane per asset. 7. Wheel zoom computes a new `start`/`end`, fetches `_partial=1`, parses the returned HTML, and replaces `#tl-canvas.innerHTML`. ## Findings ### 1. Wheel zoom is janky by design The wheel handler performs a backend request and a large DOM replacement for zoom. A sampled partial request returned about 315 KB in roughly 416 ms and contained 108 segment elements plus 225 event elements. Initial rendered `#tl-canvas` HTML was about 335 KB. The handler also throttles wheel input with a fixed 400 ms timeout. That makes zoom feel stepped and laggy, especially because each accepted wheel event waits for server render, network, HTML parsing, and full canvas replacement. Root causes: - Zoom is server-rendered instead of client-side transformed. - The whole canvas is replaced, not just scale metadata or a small diff. - No request cancellation, no stale response guard, and no loading/error state. - The wheel math uses only the current hidden `start`/`end`, not the cursor position as the zoom anchor. ### 2. Zoom can appear to lose logs/assets There are two separate problems here. First, the wheel AJAX URL preserves `q`, `scale`, `asset_type`, and `location`, but it does not preserve `asset_ids` or `record_scope`. I reproduced this by opening the timeline with two selected assets. Before wheel zoom, the page showed 2 rows. After one wheel zoom, the canvas showed 14 rows while the asset multi-select still displayed the original 2 selected assets. The UI state and rendered data diverged. Second, segment width is rendered incorrectly. The backend computes both `track_width_pct` and `body_width_pct`, but the template uses only `body_width_pct` as the absolute element width. Because `body_width_pct` is usually `100%`, each segment starts at its own left offset and then stretches to the end of the lane. Later segments visually cover earlier ones, which can look like logs disappeared or merged. Root causes: - Missing query parameters in wheel AJAX. - Partial replacement updates only the canvas, not metrics, range display, or selected filter state. - Template uses `seg.body_width_pct` where it needs the actual timeline span, likely `seg.track_width_pct` on the outer element and `seg.body_width_pct` only on an inner body. ### 3. Scale does not auto-switch between year/month/day The backend can auto-resolve scale when no `scale` query parameter is supplied. But wheel zoom always sends the current `