chore: add reference docs, scripts, tests, demo, prototypes

- Reference docs: known issues, user journey, location redesign, mission merge plan
- Scripts: fill_location_state, merge_missions, migrate_location, migrate_state
- Tests: e2e test suite, asset entries unit tests
- Demo: location board, hifi prototypes, test screenshots
- Data backup: asset log entries and state nodes
- Update .gitignore

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-01 15:29:47 +08:00
co-authored by Claude Opus 4.7
parent f4909765af
commit e1d601a774
190 changed files with 11464 additions and 0 deletions
+293
View File
@@ -0,0 +1,293 @@
# 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 `<select name="scale">` value, so the backend never gets a chance to auto-switch. I reproduced an 88-day visible range that still rendered a single `2057` year label instead of month/day labels.
There is also a template bug: only the Month option has a conditional `selected` attribute. If the route renders `scale=day`, the labels are daily, but the select still displays Year. That means the next wheel AJAX reads `scale=year` from the control and corrupts the intended scale.
Root causes:
- Manual scale and auto scale are conflated.
- Wheel zoom always sends a scale even when the user has not explicitly locked one.
- Year and Day options are missing selected-state rendering.
### 4. Filter model does not match the desired workflow
Desired behavior: first filter by type and location, then show a dropdown multi-select of matching assets, default all selected, and render only selected assets.
Current behavior:
- Type and Location are single native selects.
- Asset selection is a native multi-select with no explicit default selections. Empty selection means "all", but visually it looks like nothing is selected.
- Asset options are derived from filtered rows, but wheel zoom does not preserve selected `asset_ids`.
- State filter is not implemented.
- Location filtering is exact string matching, not hierarchical matching.
- There is no Select All / Clear affordance.
Root causes:
- The route treats asset selection as optional narrowing instead of an explicit selected set.
- Filter state is encoded inconsistently between server render, form controls, and wheel AJAX.
- Location taxonomy is still free-text-derived.
### 5. Event point display is too weak
The backend calculates event label width, label side, label span, event tracks, and state-node connector height. The templates do not render those labels, tracks, or connectors. They render only a 7 px dot at `top:3px` inside a fixed 26 px row.
Effects:
- Many event points collapse into indistinguishable dots.
- State nodes and standalone event points look nearly identical.
- Event labels are only available through hover/title, so scanning the timeline is hard.
- Track assignment does not help because row height is fixed and event track fields are unused.
Root causes:
- The presentation layer is a simplified version of a richer backend layout model.
- Fixed row height prevents multi-track event rendering.
- Event marker design has no density strategy for close events.
### 6. Asset rows do not link to asset detail
The asset name cell is a plain `div` with a `title`. Segments and event dots link to entry edit pages, not asset pages. There is no click target to navigate from a timeline row to `/assets/<asset_id>`.
Root cause:
- `_build_timeline_rows()` includes `asset_id` but not an `asset_url`, and the template does not render the label as an anchor.
## Proposed Refactor
### A. Make timeline state explicit
Create one canonical timeline state object:
- `sim_time`
- `range_start`
- `range_end`
- `scale_mode`: `auto`, `year`, `month`, or `day`
- `asset_type`
- `location`
- `state`
- `asset_ids`
- `record_scope`
- `q`
Rules:
- `scale_mode=auto` lets the backend choose year/month/day from range size.
- Explicit year/month/day locks the scale until the user returns to auto.
- Type/location/state changes rebuild the candidate asset list.
- Candidate assets default to all selected.
- Empty selected assets should mean "no assets selected", not "all", once the new selector is implemented.
### B. Split server data from interaction
Keep the server responsible for canonical data and initial render, but move wheel zoom/pan to a small client-side controller:
- Hydrate timeline rows as JSON or compact data attributes.
- Render positions from `range_start`, `range_end`, and resolved scale in the browser.
- On wheel, update range in memory immediately and re-render positions without a backend round trip.
- Debounce URL synchronization with `history.replaceState`.
- Fetch server data only when filters or simulation time change, or when the visible range moves outside loaded data if pagination/windowing is introduced.
If a full client renderer is too large for this pass, use an intermediate improvement:
- Preserve all query params in AJAX.
- Use `AbortController` to cancel stale partial requests.
- Add a request sequence id so late responses cannot overwrite newer views.
- Replace the whole canvas outer HTML, not only inner HTML, and update metrics/range display/filter hidden fields together.
Recommended path: implement the intermediate fix first, then move to client-side zoom if performance still feels poor.
### C. Fix segment and row rendering
Use the backend track model correctly:
- Render each segment wrapper with `left: seg.left_pct` and `width: seg.track_width_pct`.
- Render an inner segment body with `width: seg.body_width_pct`.
- Use `state_track_count` and `event_track_count` to compute row height.
- Preserve compact labels but do not let a segment occupy the rest of the lane by accident.
### D. Implement the desired filter UX
Filter bar layout:
- Search input.
- Type multi/single filter depending on final preference; for this requirement, type can remain a dropdown filter.
- Location dropdown filter using the future hierarchy matcher when available.
- State dropdown filter.
- Asset dropdown multi-select with checkboxes, Select All, Clear, and selected count.
Data behavior:
- On initial load, after type/location/state filters are applied, all matching assets are selected.
- User deselection is explicit and survives zoom/pan.
- URL serializes selected ids so refresh/share keeps the same view.
- The rendered rows always match the selected assets shown in the selector.
### E. Improve event point design
Recommended event visual model:
- State intervals remain colored horizontal bars.
- State nodes render as small anchored ticks connected to their parent interval, with a tiny label when space allows.
- Standalone event points render as diamond or pin markers, visually distinct from state nodes.
- Event labels use collision tracks when the scale has room; dense views collapse labels into count clusters or show labels only for selected/hovered rows.
- Hover/focus tooltip shows full title, date/time, asset, location/state, and edit link.
### F. Add asset navigation
Add `asset_url` to timeline rows and render the sticky asset label as an anchor:
- Normal click opens asset detail with current `sim_time`.
- Segment/event click keeps existing entry-edit behavior.
- Optional secondary "open asset" affordance can be added in tooltip for segments/events.
## End-to-End Verification Plan
### 1. Static and unit-level checks
- Assert `_resolve_timeline_scale(None, start, end)` returns day/month/year at the intended thresholds.
- Assert explicit `scale=year/month/day` remains locked.
- Assert timeline rows include `asset_url`.
- Assert every segment has `left_pct`, `track_width_pct`, and inner `body_width_pct` with valid percentages.
- Assert filters preserve `asset_ids`, `record_scope`, `q`, type, location, state, range, and scale mode.
### 2. Flask route tests
Use a deterministic fixture with at least:
- 4 assets across 2 types and 2 locations.
- State intervals with overlapping and non-overlapping ranges.
- Standalone event points.
- State nodes inside intervals.
- One retired asset.
Route assertions:
- `/mission-preview/timeline` renders all matching assets selected by default.
- `asset_type + location` filters shrink candidate assets.
- `asset_ids` renders only selected assets.
- `scale_mode=auto` switches from year to month to day as range shrinks.
- Partial responses, if retained, include enough metadata to update metrics and range display.
### 3. Browser E2E checks
Run against local server on `http://127.0.0.1:9000`.
Core cases:
1. Open `/mission-preview/timeline`.
2. Select type and location filters.
3. Verify the asset dropdown contains only matching assets and all are checked.
4. Uncheck one asset.
5. Verify its row disappears and checked count updates.
6. Wheel zoom in repeatedly.
7. Verify visible rows still match checked assets.
8. Verify scale auto-switches from Year to Month to Day at thresholds.
9. Verify no console errors.
10. Click a sticky asset name and verify navigation to `/assets/<id>`.
11. Go back, click a segment/event marker, and verify it still opens the entry edit page.
12. Hover/focus an event marker and verify tooltip contains title and time.
Performance cases:
- Measure accepted wheel-to-DOM-update latency with `performance.now()` in the browser.
- Target: under 50 ms for client-side zoom, or under 150 ms for AJAX fallback on the fixture dataset.
- Verify rapid wheel input does not apply stale responses out of order.
- Verify no full-page navigation happens during wheel zoom.
Visual cases:
- Capture screenshots at 1440x900 and 390x844.
- Verify event labels/markers do not incoherently overlap.
- Verify row heights remain stable and no text spills outside controls.
- Verify year/month/day labels match selected or auto-resolved scale.
### 4. Regression checks for the reported bugs
- Wheel jank: repeated wheel input updates range smoothly without waiting on full server-rendered HTML.
- Lost logs/assets: after selecting 2 assets, wheel zoom still shows exactly those 2 rows.
- Auto scale: an 88-day range shows day labels, a multi-month range shows month labels, a multi-year range shows year labels.
- Filter workflow: type/location filters rebuild the asset dropdown and default to all matching assets selected.
- Event points: standalone events and state nodes are visually distinct and readable.
- Asset navigation: clicking a timeline row label opens the asset detail page.
## Suggested Implementation Order
1. Fix template correctness: selected states for year/day scale and segment width rendering.
2. Preserve all query params in wheel AJAX and guard stale responses.
3. Add `asset_url` and clickable asset labels.
4. Replace native asset multi-select with explicit checkbox dropdown and selected-count model.
5. Add state filter and hierarchical location matching hook.
6. Render event labels/tracks/connectors using the data already computed by the backend.
7. Decide whether to keep AJAX zoom or replace it with a client-side timeline controller.
## Acceptance Criteria
- The timeline never shows a different asset set than the filter UI says is selected.
- Wheel zoom never drops `asset_ids`, `record_scope`, `q`, type, location, or state filters.
- Auto scale and the scale control agree with each other.
- State intervals render at their true clipped span.
- Event points are readable enough to scan without mandatory hover.
- Every asset row has a direct asset-detail navigation path.
- E2E tests cover the six reported issues and fail on the current implementation.