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 }}
+