From ac5141fd02bad9827451abede601666ceac31cc5 Mon Sep 17 00:00:00 2001 From: Armor00 <2654988228@qq.com> Date: Fri, 5 Jun 2026 16:19:32 +0800 Subject: [PATCH] feat: docking log management, sim_time DB-only, modal helpers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- app/routes/web.py | 146 +++++++++++++++++++++++++++----- app/static/app.js | 23 +++++ app/templates/base.html | 9 ++ app/templates/docking_logs.html | 41 +++++++++ 4 files changed, 196 insertions(+), 23 deletions(-) create mode 100644 app/templates/docking_logs.html diff --git a/app/routes/web.py b/app/routes/web.py index 3aabb0f..3591460 100644 --- a/app/routes/web.py +++ b/app/routes/web.py @@ -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//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//entries//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", []) ], diff --git a/app/static/app.js b/app/static/app.js index eedf6cd..ef11856 100644 --- a/app/static/app.js +++ b/app/static/app.js @@ -12,6 +12,20 @@ function initSidebar() { } initSidebar(); +// ─── Modal Helpers ─── +function openModal(id) { + const el = document.getElementById(id); + if (el) el.classList.add('open'); +} +function closeModal(id) { + const el = document.getElementById(id); + if (el) el.classList.remove('open'); +} +document.addEventListener('keydown', function(e) { + if (e.key !== 'Escape') return; + document.querySelectorAll('.modal-bg.open').forEach(el => el.classList.remove('open')); +}); + // ─── Global Time Control ─── function openTimeModal() { const display = document.getElementById('sim-time-display'); @@ -42,6 +56,15 @@ async function shiftTime(days) { async function resetTime() { await setSimTimeAndReload('2060-03-12 09:00'); } +function toggleTheme() { + const root = document.documentElement; + const current = root.dataset.theme || 'dark'; + const next = current === 'dark' ? 'light' : 'dark'; + root.dataset.theme = next; + root.style.colorScheme = next; + try { localStorage.setItem('ksp-theme', next); } catch(e) {} +} + async function setSimTimeAndReload(timeStr) { try { await fetch('/api/v1/sim-time', { diff --git a/app/templates/base.html b/app/templates/base.html index 3e53c55..a376877 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -79,6 +79,15 @@ 燃料换算 +
+ 控制台 +
+ + 📡API 健康检查 diff --git a/app/templates/docking_logs.html b/app/templates/docking_logs.html new file mode 100644 index 0000000..12116ed --- /dev/null +++ b/app/templates/docking_logs.html @@ -0,0 +1,41 @@ +{% extends "base.html" %} + +{% block content %} +
+
Console
+

对接日志管理

+
+ + + + + + + {% for e in events %} + + + + + + + + + {% endfor %} + +
对接时间分离时间母舰子载具备注操作
{{ e.docked_at.strftime('%Y-%m-%d %H:%M') if e.docked_at else '-' }}{{ e.undocked_at.strftime('%Y-%m-%d %H:%M') if e.undocked_at else '-' }} + {% if e.parent_asset %} + {{ e.parent_asset.name }} + {% else %}-{% endif %} + + {% if e.child_asset %} + {{ e.child_asset.name }} + {% elif e.child_label %}{{ e.child_label }} + {% else %}-{% endif %} + {{ e.dock_note or '-' }} +
+ +
+
+ +

{{ events|length }} records

+{% endblock %}