feat: docking log management, sim_time DB-only, modal helpers
- Add docking_log_list and docking_log_delete routes with dedicated page - _append_docking_log: detect existing state interval before creating new entry - _append_undocking_log: find entry by node title+at_time, accept docked_at param - Fix _entry_state_label and _entry_location to return time-correct values - Add api_set_sim_time POST endpoint; remove sim_time from all url_for() calls - _resolve_simulation_time simplified to DB-only (no URL param) - app.js: add openModal/closeModal/toggleTheme helpers - base.html: add 对接日志管理 nav item, remove Today button Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+123
-23
@@ -1303,37 +1303,66 @@ def _append_docking_log(
|
||||
note: str | None,
|
||||
) -> None:
|
||||
title = f"{child_name} docked to {parent_name}"
|
||||
entry = AssetLogEntry(
|
||||
entry_kind="state",
|
||||
title=title,
|
||||
start_at=docked_at,
|
||||
summary=note,
|
||||
asset=asset,
|
||||
)
|
||||
entry.state_nodes.append(
|
||||
AssetStateNode(
|
||||
title=title,
|
||||
detail=note,
|
||||
at_time=docked_at,
|
||||
target_location=target_location,
|
||||
state_label="Docked",
|
||||
# Check if an existing (non-dock) state interval covers the dock time
|
||||
existing = AssetLogEntry.query.filter(
|
||||
AssetLogEntry.asset_id == asset.id,
|
||||
AssetLogEntry.entry_kind == "state",
|
||||
AssetLogEntry.start_at <= docked_at,
|
||||
).filter(
|
||||
(AssetLogEntry.end_at.is_(None)) | (AssetLogEntry.end_at > docked_at)
|
||||
).filter(
|
||||
~AssetLogEntry.title.ilike("%docked to%")
|
||||
).order_by(AssetLogEntry.start_at.asc()).first()
|
||||
|
||||
if existing is not None:
|
||||
existing.state_nodes.append(
|
||||
AssetStateNode(
|
||||
title=title,
|
||||
detail=note,
|
||||
at_time=docked_at,
|
||||
target_location=target_location,
|
||||
state_label="Docked",
|
||||
)
|
||||
)
|
||||
)
|
||||
db.session.add(entry)
|
||||
else:
|
||||
entry = AssetLogEntry(
|
||||
entry_kind="state",
|
||||
title=title,
|
||||
start_at=docked_at,
|
||||
summary=note,
|
||||
asset=asset,
|
||||
)
|
||||
entry.state_nodes.append(
|
||||
AssetStateNode(
|
||||
title=title,
|
||||
detail=note,
|
||||
at_time=docked_at,
|
||||
target_location=target_location,
|
||||
state_label="Docked",
|
||||
)
|
||||
)
|
||||
db.session.add(entry)
|
||||
|
||||
|
||||
def _append_undocking_log(
|
||||
asset: Asset,
|
||||
child_name: str,
|
||||
parent_name: str,
|
||||
docked_at: datetime,
|
||||
undocked_at: datetime,
|
||||
note: str | None,
|
||||
) -> None:
|
||||
"""Record an undocking event in the asset's log timeline."""
|
||||
dock_title = f"{child_name} docked to {parent_name}"
|
||||
entry = AssetLogEntry.query.filter_by(
|
||||
asset_id=asset.id, entry_kind="state", title=dock_title
|
||||
).order_by(AssetLogEntry.start_at.desc()).first()
|
||||
# Find the entry that contains the dock node (may not be titled 'docked to')
|
||||
entry = AssetLogEntry.query.filter(
|
||||
AssetLogEntry.asset_id == asset.id,
|
||||
AssetLogEntry.entry_kind == "state",
|
||||
AssetLogEntry.state_nodes.any(
|
||||
(AssetStateNode.title == dock_title) &
|
||||
(AssetStateNode.at_time == docked_at)
|
||||
)
|
||||
).first()
|
||||
|
||||
if entry is not None:
|
||||
entry.state_nodes.append(AssetStateNode(
|
||||
@@ -4508,18 +4537,18 @@ def api_asset_undock(asset_id: object) -> object:
|
||||
_remove_undocking_log(child_asset, child_name, parent_name)
|
||||
docking_event.undocked_at = undock_time
|
||||
docking_event.undock_note = note
|
||||
_append_undocking_log(parent_asset, child_name, parent_name, undock_time, note)
|
||||
_append_undocking_log(parent_asset, child_name, parent_name, docking_event.docked_at, undock_time, note)
|
||||
if child_asset is not None:
|
||||
_append_undocking_log(child_asset, child_name, parent_name, undock_time, note)
|
||||
_append_undocking_log(child_asset, child_name, parent_name, docking_event.docked_at, undock_time, note)
|
||||
else:
|
||||
if docking_event.undocked_at is not None:
|
||||
raise FormValidationError("Already undocked")
|
||||
docking_event.undocked_at = undock_time
|
||||
docking_event.undock_note = note
|
||||
if parent_asset is not None:
|
||||
_append_undocking_log(parent_asset, child_name, parent_name, undock_time, note)
|
||||
_append_undocking_log(parent_asset, child_name, parent_name, docking_event.docked_at, undock_time, note)
|
||||
if child_asset is not None:
|
||||
_append_undocking_log(child_asset, child_name, parent_name, undock_time, note)
|
||||
_append_undocking_log(child_asset, child_name, parent_name, docking_event.docked_at, undock_time, note)
|
||||
|
||||
db.session.commit()
|
||||
except FormValidationError as exc:
|
||||
@@ -4534,6 +4563,65 @@ def api_asset_undock(asset_id: object) -> object:
|
||||
))
|
||||
|
||||
|
||||
@web_bp.get("/docking-logs")
|
||||
def docking_log_list() -> str:
|
||||
simulation_time = _resolve_simulation_time()
|
||||
simulation_time_input = _format_datetime_input(simulation_time)
|
||||
events = (
|
||||
db.session.execute(
|
||||
select(DockingEvent)
|
||||
.options(selectinload(DockingEvent.parent_asset), selectinload(DockingEvent.child_asset))
|
||||
.order_by(DockingEvent.docked_at.desc())
|
||||
).scalars().all()
|
||||
)
|
||||
return render_template(
|
||||
"docking_logs.html",
|
||||
title="对接日志管理",
|
||||
active_page="docking_logs",
|
||||
events=events,
|
||||
simulation_time_display=_format_datetime_display(simulation_time),
|
||||
simulation_time_input=simulation_time_input,
|
||||
)
|
||||
|
||||
|
||||
@web_bp.post("/docking-logs/<uuid:event_id>/delete")
|
||||
def docking_log_delete(event_id: object) -> object:
|
||||
event = db.session.get(DockingEvent, _parse_uuid_value(event_id))
|
||||
if event is None:
|
||||
flash("对接记录未找到。", "error")
|
||||
return redirect(url_for("web.docking_log_list"))
|
||||
|
||||
parent = db.session.get(Asset, event.parent_asset_id) if event.parent_asset_id else None
|
||||
child = db.session.get(Asset, event.child_asset_id) if event.child_asset_id else None
|
||||
child_name = child.name if child else (event.child_label or "External vehicle")
|
||||
parent_name = parent.name if parent else "Unknown"
|
||||
|
||||
# Remove docking log entries from both assets
|
||||
dock_title = f"{child_name} docked to {parent_name}"
|
||||
for asset in [parent, child]:
|
||||
if asset is None:
|
||||
continue
|
||||
entry = AssetLogEntry.query.filter(
|
||||
AssetLogEntry.asset_id == asset.id,
|
||||
AssetLogEntry.entry_kind == "state",
|
||||
AssetLogEntry.state_nodes.any(
|
||||
(AssetStateNode.title == dock_title) &
|
||||
(AssetStateNode.at_time == event.docked_at)
|
||||
)
|
||||
).first()
|
||||
if entry:
|
||||
nodes_to_remove = [n for n in entry.state_nodes if n.title == dock_title or 'undocked' in (n.title or '')]
|
||||
for n in nodes_to_remove:
|
||||
db.session.delete(n)
|
||||
if not entry.state_nodes:
|
||||
db.session.delete(entry)
|
||||
|
||||
db.session.delete(event)
|
||||
db.session.commit()
|
||||
flash("对接记录已删除。", "success")
|
||||
return redirect(url_for("web.docking_log_list"))
|
||||
|
||||
|
||||
@web_bp.post("/assets/<uuid:asset_id>/entries/<uuid:entry_id>/delete")
|
||||
def asset_entry_delete(asset_id: object, entry_id: object) -> object:
|
||||
asset = _load_asset_or_404(asset_id)
|
||||
@@ -4724,6 +4812,7 @@ def mission_timeline_preview() -> str:
|
||||
timeline_json_data = {
|
||||
"range_start": _format_datetime_input(range_start),
|
||||
"range_end": _format_datetime_input(range_end),
|
||||
"bucket_origin": _format_datetime_input(_timeline_bucket_start(range_start, timeline_scale_mode)),
|
||||
"scale": timeline_scale_mode,
|
||||
"bucket_count": len(timeline_scale),
|
||||
"rows": [
|
||||
@@ -4742,6 +4831,7 @@ def mission_timeline_preview() -> str:
|
||||
"start_position": float(s["start_position"]),
|
||||
"body_end_position": float(s["body_end_position"]),
|
||||
"display_end_position": float(s["display_end_position"]),
|
||||
"track": int(s.get("track", 0)),
|
||||
"start_at": str(s["start_at"]),
|
||||
"end_at": str(s["end_at"]),
|
||||
"body_color": str(s["body_color"]),
|
||||
@@ -4758,6 +4848,16 @@ def mission_timeline_preview() -> str:
|
||||
"at_iso": str(e.get("at_iso", "")),
|
||||
"full_label": str(e.get("full_label", e["label"])),
|
||||
"is_state_node": bool(e.get("is_state_node", False)),
|
||||
"track": int(e.get("track", 0)),
|
||||
"parent_entry_id": str(e.get("parent_entry_id", "")),
|
||||
"parent_track": next(
|
||||
(
|
||||
int(segment.get("track", 0))
|
||||
for segment in r.get("segments", [])
|
||||
if segment.get("entry_id") == e.get("parent_entry_id")
|
||||
),
|
||||
None,
|
||||
),
|
||||
}
|
||||
for e in r.get("events", [])
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user