diff --git a/fam-edge/src/fam_edge/api_gateway/api_gateway.py b/fam-edge/src/fam_edge/api_gateway/api_gateway.py index 2050b70..ae94e57 100644 --- a/fam-edge/src/fam_edge/api_gateway/api_gateway.py +++ b/fam-edge/src/fam_edge/api_gateway/api_gateway.py @@ -153,9 +153,20 @@ def activity(): model_calls = db._conn.execute( "SELECT id, provider, model, filename, started_at, duration_sec, " "success, error FROM model_calls ORDER BY id DESC LIMIT 5").fetchall() + # 运动片段分割状态(服务状态页「视频分割」卡) + segment = db.get_segment_status() + try: + import os + from ..config_loader import load_config + seg_dir = load_config().get('motion_segment', {}).get( + 'clips_dir', '/opt/fam-edge/motion_clips') + segment['clips_files'] = len(os.listdir(seg_dir)) if os.path.isdir(seg_dir) else 0 + except Exception: + segment['clips_files'] = 0 return jsonify({ "queue": q_status, "db": db.get_queue_status(), + "segment": segment, "rclone": _last_activity('rclone'), "person": _last_activity('person'), "model_calls": [dict(m) for m in model_calls], diff --git a/fam-edge/src/fam_edge/oracle_db.py b/fam-edge/src/fam_edge/oracle_db.py index 59e7ac7..5e42cef 100644 --- a/fam-edge/src/fam_edge/oracle_db.py +++ b/fam-edge/src/fam_edge/oracle_db.py @@ -369,6 +369,35 @@ class OracleDB: "recent": [dict(r) for r in recent], } + def get_segment_status(self) -> Dict: + """运动片段分割状态(前端服务状态页「视频分割」卡)。 + + 统计 motion_event_id 非空的运动片段记录 + 最近分割活动。 + """ + seg_sql = "FROM videos WHERE motion_event_id IS NOT NULL" + total = self._conn.execute(f"SELECT COUNT(*) c {seg_sql}").fetchone()['c'] + done = self._conn.execute( + f"SELECT COUNT(*) c {seg_sql} AND status='done'").fetchone()['c'] + pending = self._conn.execute( + f"SELECT COUNT(*) c {seg_sql} AND status='pending'").fetchone()['c'] + failed = self._conn.execute( + f"SELECT COUNT(*) c {seg_sql} AND status='failed'").fetchone()['c'] + # 已分割事件数(videos.motion_event_id 去重)与待分割事件数(ss_motion_events 未生成片段) + motion_total = self._conn.execute( + "SELECT COUNT(*) c FROM ss_motion_events WHERE event_type=10 AND COALESCE(duration,0)>0" + ).fetchone()['c'] + last = self._conn.execute( + "SELECT service, action, detail, ts FROM service_activity " + "WHERE service='segment' ORDER BY id DESC LIMIT 1").fetchone() + return { + "total": total, + "done": done, + "pending": pending, + "failed": failed, + "motion_events": motion_total, + "last": dict(last) if last else None, + } + def set_video_file_status(self, video_id: int, valid: bool, error: str = '', media_meta: dict = None): """登记/更新文件校验结果:valid / file_error / media_meta_json""" diff --git a/fam-ui/src/views/ServiceStatus.vue b/fam-ui/src/views/ServiceStatus.vue index 6b072f3..c46f03a 100644 --- a/fam-ui/src/views/ServiceStatus.vue +++ b/fam-ui/src/views/ServiceStatus.vue @@ -38,6 +38,7 @@ const qs = computed(() => queue.value.stats || {}) const rclone = computed(() => oracle.value.rclone) const person = computed(() => oracle.value.person) +const segment = computed(() => oracle.value.segment) const nasSync = computed(() => data.value?.nas_sync) const modelCalls = computed(() => oracle.value.model_calls || []) const activities = computed(() => oracle.value.activities || []) @@ -101,6 +102,15 @@ const SVC_BADGE = { 近5次 成功{{ modelCalls.filter(m => m.success).length }}/{{ modelCalls.length }} + + {{ segment && segment.total ? `运动片段 ${segment.done ?? 0}/${segment.total} · 文件 ${segment.clips_files ?? 0}` : '暂无片段' }} + +
最近活动