Add device-side log file, remote terminal, and streaming error reporting

IPTV.py now writes logs to a rotating file and forwards critical VLC
playback errors (start timeout, MediaPlayerEncounteredError) to the
server instead of losing everything to unredirected print() calls.
The web UI's device menu gets a working Terminal (to run diagnostic
commands like tail on the log) and a Recent errors view backed by a
capped last_errors list on each device document, keeping DB volume
bounded to the last 5 entries per device.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
ubuntu
2026-08-16 22:09:51 +00:00
co-authored by Claude Sonnet 5
parent 10fd5106f8
commit 3eb0d5e8cd
3 changed files with 230 additions and 60 deletions
+16
View File
@@ -74,6 +74,7 @@ recents = db_iptv['recents']
settings_collection = db_iptv['settings'] settings_collection = db_iptv['settings']
devices_db = db_iptv['devices'] devices_db = db_iptv['devices']
MAX_STREAMING_ERRORS_PER_DEVICE = 5
devices = [] devices = []
active_xtream_imports = {} active_xtream_imports = {}
sio_session_ips = {} sio_session_ips = {}
@@ -1471,6 +1472,21 @@ async def communication(fromSid, data):
await sio.emit('communication', data, room=toSidDevice ,callback=messageReceived) await sio.emit('communication', data, room=toSidDevice ,callback=messageReceived)
@sio.event
async def streaming_error(sid, data):
IdDevice = data.get('IdDevice')
message = data.get('message', '')
if not IdDevice:
return
print('streaming_error from device :', IdDevice, message, flush=True)
error_entry = {'message': str(message)[:500], 'timestamp': time.time()}
devices_db.update_one(
{'id_device': IdDevice},
{'$push': {'last_errors': {'$each': [error_entry], '$slice': -MAX_STREAMING_ERRORS_PER_DEVICE}}}
)
@sio.event @sio.event
def disconnect(sid): def disconnect(sid):
sio_session_ips.pop(sid, None) sio_session_ips.pop(sid, None)
+102 -56
View File
@@ -5,6 +5,7 @@
import logging import logging
import logging.handlers
import vlc import vlc
import os import os
import re import re
@@ -12,7 +13,6 @@ import sys
import json import json
import queue import queue
import psutil import psutil
import logging
import requests import requests
import time import time
import threading import threading
@@ -44,6 +44,31 @@ sio = socketio.Client(
) )
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
# ----------------------------------------------------------------
# LOGGING (fichier local avec rotation, consultable via le Terminal
# du menu device sur le site, ex: tail -n 200 iptv.log)
# ----------------------------------------------------------------
LOG_FILE_PATH = "/home/pi/Documents/iptv.log"
LOG_FILE_MAX_BYTES = 1_000_000
LOG_FILE_BACKUP_COUNT = 5
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
_log_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
_console_handler = logging.StreamHandler()
_console_handler.setFormatter(_log_formatter)
logger.addHandler(_console_handler)
try:
_file_handler = logging.handlers.RotatingFileHandler(
LOG_FILE_PATH, maxBytes=LOG_FILE_MAX_BYTES, backupCount=LOG_FILE_BACKUP_COUNT
)
_file_handler.setFormatter(_log_formatter)
logger.addHandler(_file_handler)
except OSError as exc:
logger.warning("Unable to open log file %s: %s", LOG_FILE_PATH, exc)
class IdleScreenManager: class IdleScreenManager:
def __init__(self): def __init__(self):
@@ -167,7 +192,7 @@ class IdleScreenManager:
self.root.after(IDLE_SCREEN_REFRESH_INTERVAL_MS, self._refresh_recent_data) self.root.after(IDLE_SCREEN_REFRESH_INTERVAL_MS, self._refresh_recent_data)
self.root.mainloop() self.root.mainloop()
except Exception as exc: except Exception as exc:
print("Unable to start idle screen:", exc, flush=True) logger.warning("Unable to start idle screen: %s", exc)
self.enabled = False self.enabled = False
def _process_commands(self): def _process_commands(self):
@@ -358,7 +383,7 @@ def fetch_channel_logo_photo(item):
try: try:
photo_image = ImageTk.PhotoImage(background) photo_image = ImageTk.PhotoImage(background)
except Exception as exc: except Exception as exc:
print("Unable to create Tk logo image:", exc, flush=True) logger.warning("Unable to create Tk logo image: %s", exc)
return None, f"Tk image creation failed: {exc}" return None, f"Tk image creation failed: {exc}"
logo_cache[cache_key] = photo_image logo_cache[cache_key] = photo_image
@@ -482,7 +507,7 @@ def sync_recent_channels_from_server():
if normalized_items: if normalized_items:
return save_recent_channels_to_db(normalized_items) return save_recent_channels_to_db(normalized_items)
except Exception as exc: except Exception as exc:
print("Unable to refresh recent channels from server:", exc, flush=True) logger.warning("Unable to refresh recent channels from server: %s", exc)
return recent return recent
@@ -494,7 +519,34 @@ def focus_vlc_player():
try: try:
media_principal.set_fullscreen(True) media_principal.set_fullscreen(True)
except Exception as exc: except Exception as exc:
print("Unable to focus VLC player:", exc, flush=True) logger.warning("Unable to focus VLC player: %s", exc)
STREAMING_ERROR_REPORT_MIN_INTERVAL_SECONDS = 5
_last_streaming_error_report_ts = 0
def report_streaming_error(message):
"""Log a critical streaming error and forward it to the server (best effort,
rate-limited so a burst of VLC errors doesn't flood the socket)."""
global _last_streaming_error_report_ts
logger.error(message)
now = time.time()
if now - _last_streaming_error_report_ts < STREAMING_ERROR_REPORT_MIN_INTERVAL_SECONDS:
return
_last_streaming_error_report_ts = now
try:
if sio.connected:
sio.emit('streaming_error', {'IdDevice': IdDevice, 'message': str(message)})
except Exception as exc:
logger.warning("Unable to report streaming error to server: %s", exc)
def on_vlc_encountered_error(event):
report_streaming_error("VLC encountered a playback error (channel: %s)" % mediaRunning)
def wait_for_vlc_playback_start(): def wait_for_vlc_playback_start():
@@ -507,11 +559,11 @@ def wait_for_vlc_playback_start():
focus_vlc_player() focus_vlc_player()
return True return True
except Exception as exc: except Exception as exc:
print("Unable to determine VLC playback state:", exc, flush=True) logger.warning("Unable to determine VLC playback state: %s", exc)
break break
time.sleep(VLC_PLAYBACK_POLL_INTERVAL_SECONDS) time.sleep(VLC_PLAYBACK_POLL_INTERVAL_SECONDS)
print("VLC did not report playback start before timeout", flush=True) report_streaming_error("VLC did not report playback start before timeout (channel: %s)" % mediaRunning)
return False return False
@@ -617,6 +669,7 @@ def new_media_player(url):
media.add_option(f":vout={forced_vout}") media.add_option(f":vout={forced_vout}")
player.set_media(media) player.set_media(media)
player.event_manager().event_attach(vlc.EventType.MediaPlayerEncounteredError, on_vlc_encountered_error)
return player return player
@@ -650,7 +703,7 @@ def capture_and_upload_snapshot():
snapshot_path = get_snapshot_filepath() snapshot_path = get_snapshot_filepath()
snapshot_status = media_principal.video_take_snapshot(0, snapshot_path, 0, 0) snapshot_status = media_principal.video_take_snapshot(0, snapshot_path, 0, 0)
if snapshot_status != 0: if snapshot_status != 0:
print('snapshot capture failed', flush=True) logger.error("Snapshot capture failed")
return False return False
upload_snapshot_file(snapshot_path) upload_snapshot_file(snapshot_path)
@@ -663,7 +716,7 @@ def snapshot_loop():
if auto_snapshot_enabled and data_running: if auto_snapshot_enabled and data_running:
capture_and_upload_snapshot() capture_and_upload_snapshot()
except Exception as exc: except Exception as exc:
print('snapshot loop error', exc, flush=True) logger.warning("Snapshot loop error: %s", exc)
time.sleep(SNAPSHOT_INTERVAL_SECONDS) time.sleep(SNAPSHOT_INTERVAL_SECONDS)
def RepresentsInt(s): def RepresentsInt(s):
@@ -677,7 +730,7 @@ def convertMillis(millis):
seconds=(millis/1000)%60 seconds=(millis/1000)%60
minutes=(millis/(1000*60))%60 minutes=(millis/(1000*60))%60
hours=(millis/(1000*60*60))%24 hours=(millis/(1000*60*60))%24
print(str(seconds)+" "+str(minutes)+" "+ str(hours)) logger.debug("%s %s %s", seconds, minutes, hours)
return seconds, minutes, hours return seconds, minutes, hours
def is_vpn_connected(): def is_vpn_connected():
@@ -730,12 +783,12 @@ def findInfo(tag, s):
def restart_process(): def restart_process():
try: try:
print('restart new process') logger.info("Restarting process")
p = psutil.Process(os.getpid()) p = psutil.Process(os.getpid())
for handler in p.get_open_files() + p.connections(): for handler in p.get_open_files() + p.connections():
os.close(handler.fd) os.close(handler.fd)
except Exception as e: except Exception as e:
logging.error(e) logger.error(e)
python = sys.executable python = sys.executable
os.execl(python, python, *sys.argv) os.execl(python, python, *sys.argv)
@@ -768,7 +821,7 @@ def display_overlay_message(message, duration_ms=10000):
media_principal.video_set_marquee_int(vlc.VideoMarqueeOption.Timeout, duration_ms) media_principal.video_set_marquee_int(vlc.VideoMarqueeOption.Timeout, duration_ms)
media_principal.video_set_marquee_int(vlc.VideoMarqueeOption.Position, 8) media_principal.video_set_marquee_int(vlc.VideoMarqueeOption.Position, 8)
except Exception as exc: except Exception as exc:
print('Unable to display overlay message:', exc, flush=True) logger.warning("Unable to display overlay message: %s", exc)
def playMedia(id_channel, url_channel, name_channel, id_playlist): def playMedia(id_channel, url_channel, name_channel, id_playlist):
@@ -781,20 +834,20 @@ def playMedia(id_channel, url_channel, name_channel, id_playlist):
media_principal.stop() media_principal.stop()
media_principal.release() media_principal.release()
except Exception as e: except Exception as e:
print("Unable to reset VLC player:", e, flush=True) logger.warning("Unable to reset VLC player: %s", e)
media_principal = new_media_player(url_channel) media_principal = new_media_player(url_channel)
try: try:
media_principal.set_fullscreen(True) media_principal.set_fullscreen(True)
except Exception as e: except Exception as e:
print("Unable to enable fullscreen:", e, flush=True) logger.warning("Unable to enable fullscreen: %s", e)
media_principal.audio_set_volume(200) media_principal.audio_set_volume(200)
media_principal.play() media_principal.play()
start_playback_transition() start_playback_transition()
mediaRunning = name_channel mediaRunning = name_channel
data_running = {'name_channel':name_channel, 'id_channel':id_channel, 'id_playlist':id_playlist, 'url_channel': url_channel, 'progress':0} data_running = {'name_channel':name_channel, 'id_channel':id_channel, 'id_playlist':id_playlist, 'url_channel': url_channel, 'progress':0}
prepend_recent_channel(id_channel, url_channel, name_channel, id_playlist) prepend_recent_channel(id_channel, url_channel, name_channel, id_playlist)
print(' DATA : ', data_running, flush=True) logger.info("Playback started: %s", data_running)
media_principal.video_set_marquee_int(vlc.VideoMarqueeOption.Enable, 1) media_principal.video_set_marquee_int(vlc.VideoMarqueeOption.Enable, 1)
media_principal.video_set_marquee_string(vlc.VideoMarqueeOption.Text, name_channel) media_principal.video_set_marquee_string(vlc.VideoMarqueeOption.Text, name_channel)
@@ -813,7 +866,7 @@ def update_source():
updated_file.write(r.content) updated_file.write(r.content)
os.replace(temp_path, path + 'IPTV.py') os.replace(temp_path, path + 'IPTV.py')
except Exception as exc: except Exception as exc:
print('error update_source', exc, flush=True) logger.error("Error during update_source: %s", exc)
if os.path.exists(temp_path): if os.path.exists(temp_path):
os.remove(temp_path) os.remove(temp_path)
raise raise
@@ -821,14 +874,14 @@ def update_source():
def wait_for_internet_connection(): def wait_for_internet_connection():
while True: while True:
print("Checking connexion ...", flush=True) logger.info("Checking connexion ...")
for check_url, verify_ssl in CONNECTIVITY_CHECK_TARGETS: for check_url, verify_ssl in CONNECTIVITY_CHECK_TARGETS:
try: try:
requests.get(check_url, timeout=CONNECTIVITY_CHECK_TIMEOUT_SECONDS, verify=verify_ssl) requests.get(check_url, timeout=CONNECTIVITY_CHECK_TIMEOUT_SECONDS, verify=verify_ssl)
print(f"Connectivity check succeeded with {check_url}", flush=True) logger.info("Connectivity check succeeded with %s", check_url)
return return
except requests.RequestException as exc: except requests.RequestException as exc:
print(f"Connectivity check failed for {check_url}: {exc}", flush=True) logger.warning("Connectivity check failed for %s: %s", check_url, exc)
time.sleep(CONNECTIVITY_CHECK_RETRY_SECONDS) time.sleep(CONNECTIVITY_CHECK_RETRY_SECONDS)
@@ -856,18 +909,18 @@ def ensure_socket_connection(force_reset=False):
pass pass
wait_for_internet_connection() wait_for_internet_connection()
print('Attempting socket connection...', flush=True) logger.info("Attempting socket connection...")
sio.connect( sio.connect(
url_update_server, url_update_server,
wait=True, wait=True,
wait_timeout=SOCKET_CONNECT_TIMEOUT, wait_timeout=SOCKET_CONNECT_TIMEOUT,
transports=['websocket', 'polling'] transports=['websocket', 'polling']
) )
print('Socket connection ready', flush=True) logger.info("Socket connection ready")
server_connection_ready = True server_connection_ready = True
return True return True
except Exception as exc: except Exception as exc:
print('Socket reconnection failed', exc, flush=True) logger.warning("Socket reconnection failed: %s", exc)
server_connection_ready = False server_connection_ready = False
return False return False
@@ -876,7 +929,7 @@ def polling_server():
global media_principal global media_principal
global data_running global data_running
global status_vpn global status_vpn
print('polling_server') logger.info("Polling loop started")
while True: while True:
try: try:
if not sio.connected: if not sio.connected:
@@ -890,7 +943,7 @@ def polling_server():
percentage = media_principal.get_position()*100 percentage = media_principal.get_position()*100
if data_running: if data_running:
print(data_running_tmp) logger.debug("Current playback state: %s", data_running_tmp)
data_running = {'name_channel':data_running_tmp['name_channel'], 'id_channel':data_running_tmp['id_channel'], 'id_playlist':data_running_tmp['id_playlist'], 'url_channel':data_running_tmp['url_channel'], 'progress':milli, 'percentage':percentage} data_running = {'name_channel':data_running_tmp['name_channel'], 'id_channel':data_running_tmp['id_channel'], 'id_playlist':data_running_tmp['id_playlist'], 'url_channel':data_running_tmp['url_channel'], 'progress':milli, 'percentage':percentage}
sio.emit('polling', { sio.emit('polling', {
'name': name_device, 'name': name_device,
@@ -902,7 +955,7 @@ def polling_server():
'snapshot_interval_seconds': SNAPSHOT_INTERVAL_SECONDS 'snapshot_interval_seconds': SNAPSHOT_INTERVAL_SECONDS
}) })
except Exception as exc: except Exception as exc:
print('Polling failed, will reconnect', exc, flush=True) logger.warning("Polling failed, will reconnect: %s", exc)
ensure_socket_connection(force_reset=True) ensure_socket_connection(force_reset=True)
time.sleep(POLLING_INTERVAL_SECONDS) time.sleep(POLLING_INTERVAL_SECONDS)
@@ -911,23 +964,23 @@ def polling_server():
def sendMessage(fromSidDevice, toSidDevice, typeCommand, payload, num_packet): def sendMessage(fromSidDevice, toSidDevice, typeCommand, payload, num_packet):
new_data = {'fromSidDevice': fromSidDevice, 'toSidDevice': toSidDevice, 'typeCommand': typeCommand, 'payload': payload, 'packet':num_packet} new_data = {'fromSidDevice': fromSidDevice, 'toSidDevice': toSidDevice, 'typeCommand': typeCommand, 'payload': payload, 'packet':num_packet}
print(new_data) logger.info("Sending message: %s", new_data)
if sio.connected or ensure_socket_connection(): if sio.connected or ensure_socket_connection():
sio.emit('communication', new_data) sio.emit('communication', new_data)
else: else:
print('Socket disconnected, unable to send message', flush=True) logger.warning("Socket disconnected, unable to send message")
@sio.event @sio.event
def connect(): def connect():
global server_connection_ready global server_connection_ready
server_connection_ready = True server_connection_ready = True
print('connection established') logger.info("Connection established")
@sio.event @sio.event
def connect_error(data): def connect_error(data):
print('connection error', data, flush=True) logger.error("Connection error: %s", data)
@sio.event @sio.event
@@ -973,14 +1026,14 @@ def communication(data):
# PLAY A CHANNEL ON # PLAY A CHANNEL ON
# -------------------------------- # --------------------------------
if typeCommand == "on": if typeCommand == "on":
print ("switch on") logger.info("Switch on requested")
data_channel = data['payload'] data_channel = data['payload']
name_channel = data_channel['name'] name_channel = data_channel['name']
url_channel = data_channel['url'] url_channel = data_channel['url']
id_channel = data_channel['id_channel'] id_channel = data_channel['id_channel']
id_playlist = data_channel['id_playlist'] id_playlist = data_channel['id_playlist']
print(id_channel, name_channel, url_channel, flush=True) logger.info("Playing channel id=%s name=%s url=%s", id_channel, name_channel, url_channel)
playMedia(id_channel, url_channel, name_channel, id_playlist) playMedia(id_channel, url_channel, name_channel, id_playlist)
sendMessage(toSidDevice, fromSidDevice, 'playing', name_channel, 0) sendMessage(toSidDevice, fromSidDevice, 'playing', name_channel, 0)
sendMessage(toSidDevice, fromSidDevice, 'warning', 'Starting media', 0) sendMessage(toSidDevice, fromSidDevice, 'warning', 'Starting media', 0)
@@ -1105,7 +1158,7 @@ def communication(data):
if typeCommand == "changeDeviceName": if typeCommand == "changeDeviceName":
global name_device global name_device
name_device = data['payload'] name_device = data['payload']
print(name_device) logger.info("Change device name to: %s", name_device)
query = "UPDATE settings SET valueParameter = '" + name_device + "' WHERE nameParameter = 'deviceName'" query = "UPDATE settings SET valueParameter = '" + name_device + "' WHERE nameParameter = 'deviceName'"
cur_ = con.cursor() cur_ = con.cursor()
cur_.execute(query) cur_.execute(query)
@@ -1134,7 +1187,7 @@ def communication(data):
# -------------------------------- # --------------------------------
if typeCommand == "takeSnapshot": if typeCommand == "takeSnapshot":
try: try:
print('take snapshot') logger.info("Take snapshot requested")
time.sleep(1) time.sleep(1)
capture_and_upload_snapshot() capture_and_upload_snapshot()
except Exception as e: except Exception as e:
@@ -1152,22 +1205,19 @@ def communication(data):
# Command line # Command line
# -------------------------------- # --------------------------------
if typeCommand == "command_line": if typeCommand == "command_line":
logger.info("Command line requested: %s", data['payload'])
records = re.split(r' ', data['payload']) records = re.split(r' ', data['payload'])
#result = subprocess.run(records, capture_output=True, text=True) process = subprocess.Popen(records, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
process = subprocess.Popen(records, stdout=subprocess.PIPE, text=True)
for line in process.stdout: for line in process.stdout:
print(line, end="") logger.info("command_line output: %s", line.rstrip())
sendMessage(toSidDevice, fromSidDevice, 'display_command_result', line, 0) sendMessage(toSidDevice, fromSidDevice, 'display_command_result', line, 0)
#print(result.stdout)
@sio.event @sio.event
def disconnect(): def disconnect():
global server_connection_ready global server_connection_ready
server_connection_ready = False server_connection_ready = False
print('Disconnected from server', flush=True) logger.warning("Disconnected from server")
@@ -1181,7 +1231,7 @@ def initDB():
cur = con.cursor() cur = con.cursor()
cur.execute('SELECT SQLITE_VERSION()') cur.execute('SELECT SQLITE_VERSION()')
data = cur.fetchone() data = cur.fetchone()
print ("Connexion succeed : SQLite version: %s" % data) logger.info("Connexion succeed : SQLite version: %s", data)
cur.execute( cur.execute(
""" """
CREATE TABLE IF NOT EXISTS recent_channels_cache ( CREATE TABLE IF NOT EXISTS recent_channels_cache (
@@ -1197,7 +1247,7 @@ def initDB():
con.commit() con.commit()
except (lite.Error, e): except (lite.Error, e):
print ("Error %s:" % e.args[0]) logger.error("Error %s:", e.args[0])
sys.exit(1) sys.exit(1)
@@ -1217,9 +1267,9 @@ def initIPTV():
record = cur_.fetchone() record = cur_.fetchone()
if record: if record:
IdDevice = record[0] IdDevice = record[0]
print(IdDevice) logger.info("Device ID: %s", IdDevice)
else: else:
print("No ID device found") logger.info("No ID device found, generating one")
IdDevice = str(uuid.uuid4()) IdDevice = str(uuid.uuid4())
query = "INSERT INTO settings (nameParameter, valueParameter) VALUES ('IdDevice', '"+IdDevice+"')" query = "INSERT INTO settings (nameParameter, valueParameter) VALUES ('IdDevice', '"+IdDevice+"')"
cur_ = con.cursor() cur_ = con.cursor()
@@ -1234,7 +1284,7 @@ def initIPTV():
valueParameter = cur_.fetchone() valueParameter = cur_.fetchone()
if valueParameter is not None: if valueParameter is not None:
name_device = valueParameter name_device = valueParameter
print('Update device name : ' + str(name_device)) logger.info("Update device name : %s", name_device)
# SAVE PID # SAVE PID
@@ -1252,7 +1302,7 @@ def initIPTV():
# ---------------------------------------------------------------- # ----------------------------------------------------------------
# ---------------------------------------------------------------- # ----------------------------------------------------------------
num_version='5.0.4' num_version='5.0.6'
# ---------------------------------------------------------------- # ----------------------------------------------------------------
# ---------------------------------------------------------------- # ----------------------------------------------------------------
@@ -1266,7 +1316,7 @@ db_file = 'config.db'
path = os.path.dirname(os.path.realpath(__file__))+'/' path = os.path.dirname(os.path.realpath(__file__))+'/'
con = None con = None
pid = os.getppid() pid = os.getppid()
print('PID : ', pid) logger.info("PID : %s", pid)
channels = [] channels = []
playlists = [] playlists = []
@@ -1317,15 +1367,11 @@ auth_file = "/home/pi/Documents/auth.txt"
# ---------------------------------------------------------------- # ----------------------------------------------------------------
# ---------------------------------------------------------------- # ---------------------------------------------------------------
# (logging configuré plus haut dans le fichier, juste après les imports)
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO
)
logger = logging.getLogger(__name__)
# ---------------------------------------------------------------- # ----------------------------------------------------------------
# LOOP PRINCIPAL # LOOP PRINCIPAL
# ---------------------------------------------------------------- # ----------------------------------------------------------------
if __name__ == '__main__': if __name__ == '__main__':
+112 -4
View File
@@ -961,6 +961,8 @@
<li><a onclick="openSnapshotIntervalSettings()" class="dropdown-item" href="#">Snapshot interval</a></li> <li><a onclick="openSnapshotIntervalSettings()" class="dropdown-item" href="#">Snapshot interval</a></li>
<li><a onclick="openGeminiKeySettings()" class="dropdown-item" href="#">Gemini API Key</a></li> <li><a onclick="openGeminiKeySettings()" class="dropdown-item" href="#">Gemini API Key</a></li>
<li><a onclick="openTerminal()" class="dropdown-item" href="#">Terminal</a></li> <li><a onclick="openTerminal()" class="dropdown-item" href="#">Terminal</a></li>
<li><a onclick="viewDeviceLogs()" class="dropdown-item" href="#">View logs</a></li>
<li><a onclick="openRecentErrors()" class="dropdown-item" href="#">Recent errors</a></li>
<li><a onclick="takeSnapshot()" class="dropdown-item" href="#">Take Snapshot</a></li> <li><a onclick="takeSnapshot()" class="dropdown-item" href="#">Take Snapshot</a></li>
<li><a onclick="getIP()" class="dropdown-item" href="#">Get IP</a></li> <li><a onclick="getIP()" class="dropdown-item" href="#">Get IP</a></li>
<li class="dropdown-submenu"> <li class="dropdown-submenu">
@@ -1152,19 +1154,35 @@
<!-- ZONE MODAL TERMINAL --> <!-- ZONE MODAL TERMINAL -->
<div class="modal fade" id="modalTerminal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true"> <div class="modal fade" id="modalTerminal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-sm" role="document"> <div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title">Terminal </h5> <h5 class="modal-title">Terminal </h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="input-group mb-3"> <div class="input-group mb-3">
<span class="input-group-text" id="basic-addon1">Command</span> <span class="input-group-text" id="basic-addon1">Command</span>
<input type="text" class="form-control" placeholder="Text Command" aria-label="Command" aria-describedby="basic-addon1" id="txt_command"> <input type="text" class="form-control" placeholder="Text Command" aria-label="Command" aria-describedby="basic-addon1" id="txt_command">
</div> </div>
<button type="button" class="btn btn-primary">Send command</button> <button type="button" class="btn btn-primary" id="btn_send_terminal_command">Send command</button>
<div class="alert alert-secondary" role="alert" id="txt_result_command"> <button type="button" class="btn btn-outline-secondary" id="btn_view_device_logs">View streaming logs</button>
</div> <pre class="alert alert-secondary mt-3 mb-0" role="alert" id="txt_result_command" style="max-height: 320px; overflow-y: auto; white-space: pre-wrap; text-align: left;"></pre>
</div>
</div>
</div>
</div>
<!-- ZONE MODAL RECENT ERRORS -->
<div class="modal fade" id="modalRecentErrors" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Recent streaming errors</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div id="recent_errors_list" style="max-height: 320px; overflow-y: auto;"></div>
</div> </div>
</div> </div>
</div> </div>
@@ -1511,6 +1529,7 @@
var playlistImportProgress = 0 var playlistImportProgress = 0
var playlistImportInFlight = false var playlistImportInFlight = false
var selectedDeviceSnapshotIntervalSeconds = 10 var selectedDeviceSnapshotIntervalSeconds = 10
var currentSelectedDeviceData = null
var socket = io('https://iptv.mrk.ovh', { var socket = io('https://iptv.mrk.ovh', {
secure: true, secure: true,
@@ -1549,6 +1568,13 @@
updateListDevices() updateListDevices()
} }
// RECEPTION TERMINAL COMMAND OUTPUT
if (data['typeCommand'] == 'display_command_result') {
var resultBox = document.getElementById('txt_result_command');
$(resultBox).text($(resultBox).text() + data['payload']);
resultBox.scrollTop = resultBox.scrollHeight;
}
// PROGRESS BAR DISPLAY // PROGRESS BAR DISPLAY
if (data['typeCommand'] == 'progress_analysis') { if (data['typeCommand'] == 'progress_analysis') {
$('#progress-container').show() $('#progress-container').show()
@@ -2661,6 +2687,7 @@
if (isCurrentDevice) { if (isCurrentDevice) {
currentDeviceUpdated = true; currentDeviceUpdated = true;
selectedDeviceSnapshotData = dataJSON[i]; selectedDeviceSnapshotData = dataJSON[i];
currentSelectedDeviceData = dataJSON[i];
$('#sidDevice').val( dataJSON[i].sid) $('#sidDevice').val( dataJSON[i].sid)
var data_running = dataJSON[i].data_running || {} var data_running = dataJSON[i].data_running || {}
@@ -2684,6 +2711,7 @@
} }
if (!currentDeviceUpdated) { if (!currentDeviceUpdated) {
currentSelectedDeviceData = null;
updateCurrentDeviceStream({label: 'No data', className: 'bg-secondary'}, false); updateCurrentDeviceStream({label: 'No data', className: 'bg-secondary'}, false);
updateSelectedDeviceSnapshot(null, false); updateSelectedDeviceSnapshot(null, false);
updateAutoSnapshotToggle(null); updateAutoSnapshotToggle(null);
@@ -3170,6 +3198,86 @@
}); });
}); });
// --- TERMINAL / DEVICE LOGS ---
var DEVICE_LOG_FILE_PATH = '/home/pi/Documents/iptv.log';
function openTerminal() {
if ($('#sidDevice').val().length === 0 || $('#id_device').val().length === 0) {
$('#noDeviceModal').modal('show');
return;
}
$('#txt_command').val('');
$('#txt_result_command').text('');
$('#modalTerminal').modal('show');
}
function sendTerminalCommand(command) {
if ($('#sidDevice').val().length === 0 || $('#id_device').val().length === 0) {
$('#modalTerminal').modal('hide');
$('#noDeviceModal').modal('show');
return;
}
var cmd = (typeof command === 'string') ? command : $('#txt_command').val().trim();
if (!cmd) {
return;
}
$('#txt_command').val(cmd);
$('#txt_result_command').text('');
socket.emit('communication', {
fromSidDevice: $('#sidClient').val(),
toSidDevice: $('#sidDevice').val(),
typeCommand: 'command_line',
payload: cmd,
packet: 0
});
}
$('#btn_send_terminal_command').click(function () {
sendTerminalCommand();
});
$('#btn_view_device_logs').click(function () {
sendTerminalCommand('tail -n 200 ' + DEVICE_LOG_FILE_PATH);
});
function viewDeviceLogs() {
if ($('#sidDevice').val().length === 0 || $('#id_device').val().length === 0) {
$('#noDeviceModal').modal('show');
return;
}
$('#txt_result_command').text('');
$('#modalTerminal').modal('show');
sendTerminalCommand('tail -n 200 ' + DEVICE_LOG_FILE_PATH);
}
$('#txt_command').on('keypress', function (e) {
if (e.which === 13) {
sendTerminalCommand();
}
});
function openRecentErrors() {
if ($('#sidDevice').val().length === 0 || $('#id_device').val().length === 0) {
$('#noDeviceModal').modal('show');
return;
}
var errors = (currentSelectedDeviceData && currentSelectedDeviceData.last_errors) || [];
var html = '';
if (errors.length === 0) {
html = '<div class="text-muted">No recent errors.</div>';
} else {
errors.slice().reverse().forEach(function (err) {
var when = err.timestamp ? new Date(err.timestamp * 1000).toLocaleString() : '';
html += '<div class="alert alert-danger py-2 mb-2"><strong>' + when + '</strong><br>' + $('<div>').text(err.message).html() + '</div>';
});
}
$('#recent_errors_list').html(html);
$('#modalRecentErrors').modal('show');
}
// --- AI SEARCH --- // --- AI SEARCH ---
function openAiSearch() { function openAiSearch() {
$('#aiSearchAlert').html(''); $('#aiSearchAlert').html('');