no-name-here commited on
Commit
bd989f9
Β·
verified Β·
1 Parent(s): 97cbdf6

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +155 -33
app.py CHANGED
@@ -1,13 +1,16 @@
1
  """
2
- Advanced Stream Bot β€” v3.1.0 "Final Edition"
3
- - Easy navigation: commands first, inline buttons as supplements
4
- - Full advanced settings: every parameter configurable
5
- - Default/restore mode
6
- - Anti-freeze: stream error recovery + watchdog
7
- - New message on command response (no stale edits)
8
- - Auto stream-state change notifications
9
- - Context-aware keyboards (streaming vs idle)
10
- - Improved UX flows
 
 
 
11
  """
12
 
13
  import logging
@@ -34,7 +37,7 @@ from apscheduler.schedulers.background import BackgroundScheduler
34
  # ──────────────────────────────────────────────
35
  # VERSION & APP
36
  # ──────────────────────────────────────────────
37
- APP_VERSION = "3.1.0"
38
 
39
  app = FastAPI(title="Advanced Stream Bot", version=APP_VERSION)
40
  scheduler = BackgroundScheduler(timezone="UTC")
@@ -482,6 +485,8 @@ def _do_live_view_edit(chat_id: int, force: bool = False):
482
  """
483
  Build fresh text and enqueue an editMessageText for the live-view bubble.
484
  Skips if paused (unless force=True) or if text hasn't changed.
 
 
485
  Also pushes SSE event (for web dashboard clients).
486
  """
487
  with _live_view_lock:
@@ -509,9 +514,18 @@ def _do_live_view_edit(chat_id: int, force: bool = False):
509
  if chat_id in _live_views:
510
  _live_views[chat_id]["last_sent"] = new_text
511
 
512
- # Enqueue editMessageText β€” delivered on next webhook response
513
- enqueue_outbound_message(chat_id,
514
- edit_message_text(chat_id, msg_id, new_text, reply_markup=kb))
 
 
 
 
 
 
 
 
 
515
 
516
  # Also push SSE snapshot for web clients
517
  _push_state_sse(chat_id)
@@ -521,8 +535,8 @@ def _live_view_updater_loop():
521
  """
522
  Background thread β€” fires every LIVE_VIEW_UPDATE_INTERVAL seconds.
523
  For each registered live-view, calls _do_live_view_edit().
524
- Also handles auto-unregister on terminal states and SSE for active
525
- sessions that don't have a live-view registered (scheduler-triggered).
526
  Inspired directly by logg.py's stream_command() update-interval pattern.
527
  """
528
  while True:
@@ -538,11 +552,12 @@ def _live_view_updater_loop():
538
 
539
  _do_live_view_edit(chat_id)
540
 
541
- # Auto-unregister on terminal state (leave one final edit queued)
542
  if state in ('idle', 'stopped', 'completed', 'error'):
543
- # Force a final update showing terminal state, then stop
544
  _do_live_view_edit(chat_id, force=True)
545
  unregister_live_view(chat_id)
 
 
546
 
547
  except Exception as e_inner:
548
  logger.warning(f"Live view updater error for {chat_id}: {e_inner}")
@@ -612,6 +627,7 @@ def get_main_keyboard(session: dict):
612
  if session.get('loop_count', 0) == -1:
613
  btns.append([{"text": "⏳ Finish After This Loop", "callback_data": "stream_stop_graceful"}])
614
  btns.append([{"text": "❔ Help", "callback_data": "show_help"}])
 
615
  else:
616
  # Idle mode β€” full navigation
617
  btns = []
@@ -639,6 +655,10 @@ def get_main_keyboard(session: dict):
639
  {"text": "πŸ”„ Reset to Defaults", "callback_data": "confirm_reset"},
640
  {"text": "❔ Help", "callback_data": "show_help"},
641
  ])
 
 
 
 
642
 
643
  return {"inline_keyboard": btns}
644
 
@@ -956,8 +976,9 @@ def watchdog_thread_target(chat_id: int):
956
  time.sleep(5)
957
  with lock:
958
  state = session.get('streaming_state')
959
- if state not in ("streaming", "paused", "reconnecting"):
960
- break # Stream ended, watchdog exits
 
961
 
962
  last_frame = session.get('last_frame_time')
963
  thread_ref = session.get('stream_thread_ref')
@@ -966,7 +987,8 @@ def watchdog_thread_target(chat_id: int):
966
  if thread_ref and not thread_ref.is_alive():
967
  with lock:
968
  cur_state = session.get('streaming_state')
969
- if cur_state in ("streaming", "paused", "reconnecting"):
 
970
  logger.error(f"[Chat {chat_id}] Watchdog: stream thread died unexpectedly!")
971
  append_user_live_log(chat_id, "⚠️ Stream thread died unexpectedly. Marking as error.")
972
  with lock:
@@ -980,12 +1002,12 @@ def watchdog_thread_target(chat_id: int):
980
  stall_seconds = (datetime.datetime.now() - last_frame).total_seconds()
981
  if stall_seconds > STALL_THRESHOLD:
982
  append_user_live_log(chat_id, f"⚠️ Watchdog: No frames for {stall_seconds:.0f}s β€” possible freeze!")
983
- push_message(
984
  chat_id,
985
  f"⚠️ <b>Stream may be frozen!</b>\nNo frames for <code>{stall_seconds:.0f}s</code>.\n"
986
  f"Use /abort to stop or wait for auto-reconnect.",
987
  reply_markup=get_main_keyboard(session)
988
- )
989
 
990
  logger.info(f"[Chat {chat_id}] Watchdog exiting.")
991
 
@@ -1271,10 +1293,13 @@ def stream_engine_thread_target(chat_id: int):
1271
 
1272
  except Exception as e_input:
1273
  tb = traceback.format_exc()
1274
- err_msg = f"{e_input}"
 
 
 
1275
  append_user_live_log(chat_id, f"Error on input {current_input_url}: {err_msg}")
1276
  with lock:
1277
- session['error_notification_user'] = err_msg
1278
 
1279
  with lock:
1280
  should_stop_now = session['streaming_state'] == "stopping"
@@ -1290,7 +1315,8 @@ def stream_engine_thread_target(chat_id: int):
1290
  with lock:
1291
  session['reconnect_attempt'] = cur_attempt + 1
1292
  old_st = session['streaming_state']
1293
- session['streaming_state'] = "reconnecting"
 
1294
  append_user_live_log(chat_id, f"Reconnecting in {reconnect_delay}s (attempt {cur_attempt+1}/{max_att})…")
1295
  if old_st != "reconnecting":
1296
  notify_state_change(chat_id, "reconnecting", f"Attempt {cur_attempt+1}/{max_att}. Error: {esc(err_msg)}")
@@ -1298,7 +1324,15 @@ def stream_engine_thread_target(chat_id: int):
1298
  if _input_container:
1299
  try: _input_container.close()
1300
  except Exception: pass
1301
- time.sleep(reconnect_delay)
 
 
 
 
 
 
 
 
1302
  # Retry same URL (re-enter inner loop iteration)
1303
  continue
1304
  elif stop_on_err:
@@ -1372,15 +1406,18 @@ def stream_engine_thread_target(chat_id: int):
1372
  cur = session['streaming_state']
1373
  if cur == "stopping":
1374
  final_state = "stopped"
 
1375
  elif cur == "error":
1376
  final_state = "error"
1377
  elif session.get('error_notification_user'):
1378
  final_state = "error"
1379
  else:
1380
  final_state = "completed"
 
1381
  session['streaming_state'] = final_state
1382
  session['stream_thread_ref'] = None
1383
  session['watchdog_thread_ref'] = None
 
1384
 
1385
  append_user_live_log(chat_id, f"Stream ended. Final state: {final_state}.")
1386
  notify_state_change(chat_id, final_state,
@@ -1493,19 +1530,87 @@ async def resume_stream_handler(chat_id: int, message_id: int = None):
1493
  return _reply(f"ℹ️ Cannot resume β€” state is <code>{st}</code>.", get_main_keyboard(session))
1494
 
1495
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1496
  async def abort_stream_handler(chat_id: int, message_id: int = None):
1497
  session = get_user_session(chat_id)
1498
  lock = session_locks[chat_id]
1499
  thread_ref = None
1500
 
1501
  with lock:
1502
- if session['streaming_state'] in ("streaming", "paused", "starting", "reconnecting"):
 
1503
  session['streaming_state'] = "stopping"
 
1504
  thread_ref = session.get('stream_thread_ref')
1505
  aborted = True
1506
  else:
1507
  aborted = False
1508
- st = session['streaming_state']
1509
 
1510
  def _reply(text, kb):
1511
  if message_id:
@@ -1517,16 +1622,21 @@ async def abort_stream_handler(chat_id: int, message_id: int = None):
1517
 
1518
  append_user_live_log(chat_id, "Abort requested by user.")
1519
  if thread_ref and thread_ref.is_alive():
1520
- thread_ref.join(timeout=8.0)
1521
  if thread_ref.is_alive():
1522
- append_user_live_log(chat_id, "Warning: thread did not stop cleanly.")
1523
 
1524
  with lock:
1525
- if session['streaming_state'] == "stopping":
 
1526
  session['streaming_state'] = "stopped"
1527
  session['stream_thread_ref'] = None
 
 
 
1528
 
1529
- return _reply("⏹ <b>Stream aborted.</b>\n\n" + compose_status_message(chat_id),
 
1530
  get_main_keyboard(session))
1531
 
1532
 
@@ -2027,7 +2137,8 @@ def get_help_text() -> str:
2027
  " /pause β€” Pause stream\n"
2028
  " /resume β€” Resume stream\n"
2029
  " /abort β€” Stop stream\n"
2030
- " /status β€” Detailed status\n\n"
 
2031
  "<b>━━ Configuration ━━</b>\n"
2032
  " /set β€” List all settings\n"
2033
  " /set &lt;field&gt; &lt;value&gt; β€” Set a value\n"
@@ -2055,6 +2166,10 @@ def get_help_text() -> str:
2055
  "<b>━━ Diagnostics ━━</b>\n"
2056
  " /logs β€” Recent user logs\n"
2057
  " /globallogs β€” System logs\n\n"
 
 
 
 
2058
  "<b>⚠️ Data is in-memory only β€” lost on restart.</b>"
2059
  )
2060
 
@@ -2132,6 +2247,9 @@ async def _handle_update_inner(update: dict):
2132
  elif command == "/abort":
2133
  return await abort_stream_handler(chat_id)
2134
 
 
 
 
2135
  elif command == "/status":
2136
  return send_message(chat_id, compose_status_message(chat_id, include_config=True),
2137
  reply_markup=get_main_keyboard(session))
@@ -2243,6 +2361,10 @@ async def _handle_update_inner(update: dict):
2243
  mid = message_id if ux == "edit" else None
2244
  return [ack, await abort_stream_handler(chat_id, message_id=mid)]
2245
 
 
 
 
 
2246
  elif data == "stream_status":
2247
  # Register this message as the live-view bubble (status+log mode)
2248
  # Background thread will edit it every 2s β€” exactly like logg.py
 
1
  """
2
+ Advanced Stream Bot β€” v3.2.0
3
+ - Live view: status+log bubble auto-edits every 2s (logg.py pattern)
4
+ - Pause/Resume/Refresh/Close live view controls
5
+ - Force Reboot button + /reboot command β€” last-resort recovery
6
+ - Fixed: error field no longer shows raw log lines
7
+ - Fixed: error cleared on clean stop/abort
8
+ - Fixed: stats (uptime/frames/bytes) update correctly in live view
9
+ - Fixed: abort works instantly during reconnect sleep
10
+ - Fixed: watchdog does not mark error after intentional stop
11
+ - Fixed: stale edits never pile up in outbound queue (cap=1 per message)
12
+ - SSE /events/{chat_id} for real-time web dashboard
13
+ - Scheduler-triggered streams queue outbound notifications
14
  """
15
 
16
  import logging
 
37
  # ──────────────────────────────────────────────
38
  # VERSION & APP
39
  # ──────────────────────────────────────────────
40
+ APP_VERSION = "3.2.0"
41
 
42
  app = FastAPI(title="Advanced Stream Bot", version=APP_VERSION)
43
  scheduler = BackgroundScheduler(timezone="UTC")
 
485
  """
486
  Build fresh text and enqueue an editMessageText for the live-view bubble.
487
  Skips if paused (unless force=True) or if text hasn't changed.
488
+ IMPORTANT: Replaces any existing queued edit for this message (cap=1)
489
+ so stale edits never pile up in the outbound queue.
490
  Also pushes SSE event (for web dashboard clients).
491
  """
492
  with _live_view_lock:
 
514
  if chat_id in _live_views:
515
  _live_views[chat_id]["last_sent"] = new_text
516
 
517
+ edit_payload = edit_message_text(chat_id, msg_id, new_text, reply_markup=kb)
518
+
519
+ # Replace (not append) any existing live-view edit in the queue so we never
520
+ # pile up stale edits that Telegram would reject or deliver out of order.
521
+ with _outbound_queue_lock:
522
+ existing = _outbound_message_queue.get(chat_id, [])
523
+ # Remove any previous edit targeting the same message_id
524
+ filtered = [m for m in existing
525
+ if not (m.get("method") == "editMessageText"
526
+ and m.get("message_id") == msg_id)]
527
+ filtered.append(edit_payload)
528
+ _outbound_message_queue[chat_id] = filtered
529
 
530
  # Also push SSE snapshot for web clients
531
  _push_state_sse(chat_id)
 
535
  """
536
  Background thread β€” fires every LIVE_VIEW_UPDATE_INTERVAL seconds.
537
  For each registered live-view, calls _do_live_view_edit().
538
+ On terminal state: does one forced final update then unregisters.
539
+ Also pushes SSE for active sessions without a live-view registered.
540
  Inspired directly by logg.py's stream_command() update-interval pattern.
541
  """
542
  while True:
 
552
 
553
  _do_live_view_edit(chat_id)
554
 
555
+ # Auto-unregister on terminal state after one forced final update
556
  if state in ('idle', 'stopped', 'completed', 'error'):
 
557
  _do_live_view_edit(chat_id, force=True)
558
  unregister_live_view(chat_id)
559
+ # Push final SSE
560
+ _push_state_sse(chat_id)
561
 
562
  except Exception as e_inner:
563
  logger.warning(f"Live view updater error for {chat_id}: {e_inner}")
 
627
  if session.get('loop_count', 0) == -1:
628
  btns.append([{"text": "⏳ Finish After This Loop", "callback_data": "stream_stop_graceful"}])
629
  btns.append([{"text": "❔ Help", "callback_data": "show_help"}])
630
+ btns.append([{"text": "⚑ Force Reboot", "callback_data": "force_reboot"}])
631
  else:
632
  # Idle mode β€” full navigation
633
  btns = []
 
655
  {"text": "πŸ”„ Reset to Defaults", "callback_data": "confirm_reset"},
656
  {"text": "❔ Help", "callback_data": "show_help"},
657
  ])
658
+ # Row 5: Emergency
659
+ btns.append([
660
+ {"text": "⚑ Force Reboot", "callback_data": "force_reboot"},
661
+ ])
662
 
663
  return {"inline_keyboard": btns}
664
 
 
976
  time.sleep(5)
977
  with lock:
978
  state = session.get('streaming_state')
979
+ # Exit watchdog if stream is in any terminal or stopping state
980
+ if state not in ("streaming", "paused", "reconnecting", "starting"):
981
+ break
982
 
983
  last_frame = session.get('last_frame_time')
984
  thread_ref = session.get('stream_thread_ref')
 
987
  if thread_ref and not thread_ref.is_alive():
988
  with lock:
989
  cur_state = session.get('streaming_state')
990
+ # Only mark error if not already in a clean terminal/stopping state
991
+ if cur_state in ("streaming", "paused", "reconnecting", "starting"):
992
  logger.error(f"[Chat {chat_id}] Watchdog: stream thread died unexpectedly!")
993
  append_user_live_log(chat_id, "⚠️ Stream thread died unexpectedly. Marking as error.")
994
  with lock:
 
1002
  stall_seconds = (datetime.datetime.now() - last_frame).total_seconds()
1003
  if stall_seconds > STALL_THRESHOLD:
1004
  append_user_live_log(chat_id, f"⚠️ Watchdog: No frames for {stall_seconds:.0f}s β€” possible freeze!")
1005
+ enqueue_outbound_message(chat_id, send_message(
1006
  chat_id,
1007
  f"⚠️ <b>Stream may be frozen!</b>\nNo frames for <code>{stall_seconds:.0f}s</code>.\n"
1008
  f"Use /abort to stop or wait for auto-reconnect.",
1009
  reply_markup=get_main_keyboard(session)
1010
+ ))
1011
 
1012
  logger.info(f"[Chat {chat_id}] Watchdog exiting.")
1013
 
 
1293
 
1294
  except Exception as e_input:
1295
  tb = traceback.format_exc()
1296
+ err_msg = str(e_input)
1297
+ # Strip any log-format prefix that might have leaked in
1298
+ if "] " in err_msg and "[" in err_msg[:40]:
1299
+ err_msg = err_msg.split("] ", 1)[-1] if "] " in err_msg else err_msg
1300
  append_user_live_log(chat_id, f"Error on input {current_input_url}: {err_msg}")
1301
  with lock:
1302
+ session['error_notification_user'] = err_msg[:300]
1303
 
1304
  with lock:
1305
  should_stop_now = session['streaming_state'] == "stopping"
 
1315
  with lock:
1316
  session['reconnect_attempt'] = cur_attempt + 1
1317
  old_st = session['streaming_state']
1318
+ if session['streaming_state'] not in ('stopping',):
1319
+ session['streaming_state'] = "reconnecting"
1320
  append_user_live_log(chat_id, f"Reconnecting in {reconnect_delay}s (attempt {cur_attempt+1}/{max_att})…")
1321
  if old_st != "reconnecting":
1322
  notify_state_change(chat_id, "reconnecting", f"Attempt {cur_attempt+1}/{max_att}. Error: {esc(err_msg)}")
 
1324
  if _input_container:
1325
  try: _input_container.close()
1326
  except Exception: pass
1327
+ # Sleep in small increments so abort flag is checked promptly
1328
+ for _ in range(int(reconnect_delay * 5)):
1329
+ with lock:
1330
+ if session['streaming_state'] == 'stopping':
1331
+ break
1332
+ time.sleep(0.2)
1333
+ with lock:
1334
+ if session['streaming_state'] == 'stopping':
1335
+ break
1336
  # Retry same URL (re-enter inner loop iteration)
1337
  continue
1338
  elif stop_on_err:
 
1406
  cur = session['streaming_state']
1407
  if cur == "stopping":
1408
  final_state = "stopped"
1409
+ session['error_notification_user'] = "" # clear error on clean stop
1410
  elif cur == "error":
1411
  final_state = "error"
1412
  elif session.get('error_notification_user'):
1413
  final_state = "error"
1414
  else:
1415
  final_state = "completed"
1416
+ session['error_notification_user'] = ""
1417
  session['streaming_state'] = final_state
1418
  session['stream_thread_ref'] = None
1419
  session['watchdog_thread_ref'] = None
1420
+ session['stop_gracefully_flag'] = False
1421
 
1422
  append_user_live_log(chat_id, f"Stream ended. Final state: {final_state}.")
1423
  notify_state_change(chat_id, final_state,
 
1530
  return _reply(f"ℹ️ Cannot resume β€” state is <code>{st}</code>.", get_main_keyboard(session))
1531
 
1532
 
1533
+ async def force_reboot_handler(chat_id: int, message_id: int = None):
1534
+ """
1535
+ Force Reboot β€” last-resort recovery tool.
1536
+ Kills any running stream thread, wipes ALL runtime state back to defaults
1537
+ while preserving user settings (URLs, codec, etc.), clears all queues,
1538
+ unregisters the live view, and returns the bot to a clean idle state.
1539
+ """
1540
+ session = get_user_session(chat_id)
1541
+ lock = session_locks[chat_id]
1542
+
1543
+ append_user_live_log(chat_id, "⚑ Force Reboot initiated by user.")
1544
+
1545
+ # 1. Signal any running thread to stop
1546
+ with lock:
1547
+ session['streaming_state'] = "stopping"
1548
+ session['stop_gracefully_flag'] = True
1549
+ thread_ref = session.get('stream_thread_ref')
1550
+
1551
+ # 2. Wait briefly for thread to die
1552
+ if thread_ref and thread_ref.is_alive():
1553
+ thread_ref.join(timeout=5.0)
1554
+
1555
+ # 3. Hard-close any open PyAV containers
1556
+ try:
1557
+ with lock:
1558
+ objs = session.get('pyav_objects', {})
1559
+ for key in ('output_container', 'input_container'):
1560
+ c = objs.get(key)
1561
+ if c:
1562
+ try: c.close()
1563
+ except Exception: pass
1564
+ objs[key] = None
1565
+ objs['video_out_stream'] = None
1566
+ objs['audio_out_stream'] = None
1567
+ objs['logo_image_pil'] = None
1568
+ except Exception as e:
1569
+ logger.warning(f"[Reboot] PyAV close error: {e}")
1570
+
1571
+ # 4. Reset ALL runtime state β€” keep user settings intact
1572
+ with lock:
1573
+ for k, v in DEFAULT_SESSION_RUNTIME_STATE.items():
1574
+ session[k] = dict(v) if isinstance(v, dict) else (list(v) if isinstance(v, list) else v)
1575
+
1576
+ # 5. Clear queues and live view
1577
+ unregister_live_view(chat_id)
1578
+ with _outbound_queue_lock:
1579
+ _outbound_message_queue.pop(chat_id, None)
1580
+
1581
+ _push_state_sse(chat_id)
1582
+ append_user_live_log(chat_id, "Force Reboot complete β€” bot is in clean idle state.")
1583
+
1584
+ reply_text = (
1585
+ "⚑ <b>Force Reboot Complete!</b>\n\n"
1586
+ "All stream threads killed, state wiped to clean idle.\n"
1587
+ "Your settings (URLs, codec, etc.) are preserved.\n\n"
1588
+ + compose_status_message(chat_id, include_config=False)
1589
+ )
1590
+
1591
+ def _reply(text, kb):
1592
+ if message_id:
1593
+ return edit_message_text(chat_id, message_id, text, reply_markup=kb)
1594
+ return send_message(chat_id, text, reply_markup=kb)
1595
+
1596
+ return _reply(reply_text, get_main_keyboard(session))
1597
+
1598
+
1599
  async def abort_stream_handler(chat_id: int, message_id: int = None):
1600
  session = get_user_session(chat_id)
1601
  lock = session_locks[chat_id]
1602
  thread_ref = None
1603
 
1604
  with lock:
1605
+ state = session['streaming_state']
1606
+ if state in ("streaming", "paused", "starting", "reconnecting", "stopping"):
1607
  session['streaming_state'] = "stopping"
1608
+ session['stop_gracefully_flag'] = True
1609
  thread_ref = session.get('stream_thread_ref')
1610
  aborted = True
1611
  else:
1612
  aborted = False
1613
+ st = state
1614
 
1615
  def _reply(text, kb):
1616
  if message_id:
 
1622
 
1623
  append_user_live_log(chat_id, "Abort requested by user.")
1624
  if thread_ref and thread_ref.is_alive():
1625
+ thread_ref.join(timeout=10.0)
1626
  if thread_ref.is_alive():
1627
+ append_user_live_log(chat_id, "Warning: thread did not stop cleanly in 10s β€” forcing.")
1628
 
1629
  with lock:
1630
+ # Force stopped state regardless of what thread set it to
1631
+ if session['streaming_state'] in ("stopping", "reconnecting", "starting", "streaming", "paused"):
1632
  session['streaming_state'] = "stopped"
1633
  session['stream_thread_ref'] = None
1634
+ session['watchdog_thread_ref'] = None
1635
+ session['stop_gracefully_flag'] = False
1636
+ session['error_notification_user'] = "" # clear error on clean stop
1637
 
1638
+ _push_state_sse(chat_id)
1639
+ return _reply("⏹ <b>Stream stopped.</b>\n\n" + compose_status_message(chat_id),
1640
  get_main_keyboard(session))
1641
 
1642
 
 
2137
  " /pause β€” Pause stream\n"
2138
  " /resume β€” Resume stream\n"
2139
  " /abort β€” Stop stream\n"
2140
+ " /status β€” Detailed status\n"
2141
+ " /reboot β€” ⚑ Force Reboot (last resort if bot stops responding)\n\n"
2142
  "<b>━━ Configuration ━━</b>\n"
2143
  " /set β€” List all settings\n"
2144
  " /set &lt;field&gt; &lt;value&gt; β€” Set a value\n"
 
2166
  "<b>━━ Diagnostics ━━</b>\n"
2167
  " /logs β€” Recent user logs\n"
2168
  " /globallogs β€” System logs\n\n"
2169
+ "<b>━━ Emergency ━━</b>\n"
2170
+ " /reboot β€” Kills all threads, resets state to idle.\n"
2171
+ " Use if the bot stops responding or gets stuck.\n"
2172
+ " Your URL/settings are preserved.\n\n"
2173
  "<b>⚠️ Data is in-memory only β€” lost on restart.</b>"
2174
  )
2175
 
 
2247
  elif command == "/abort":
2248
  return await abort_stream_handler(chat_id)
2249
 
2250
+ elif command in ("/reboot", "/forcereboot"):
2251
+ return await force_reboot_handler(chat_id)
2252
+
2253
  elif command == "/status":
2254
  return send_message(chat_id, compose_status_message(chat_id, include_config=True),
2255
  reply_markup=get_main_keyboard(session))
 
2361
  mid = message_id if ux == "edit" else None
2362
  return [ack, await abort_stream_handler(chat_id, message_id=mid)]
2363
 
2364
+ elif data == "force_reboot":
2365
+ return [answer_callback_query(cq["id"], "⚑ Force rebooting…"),
2366
+ await force_reboot_handler(chat_id, message_id=message_id)]
2367
+
2368
  elif data == "stream_status":
2369
  # Register this message as the live-view bubble (status+log mode)
2370
  # Background thread will edit it every 2s β€” exactly like logg.py