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:
co-authored by
Claude Sonnet 5
parent
10fd5106f8
commit
3eb0d5e8cd
+102
-56
@@ -5,6 +5,7 @@
|
||||
|
||||
|
||||
import logging
|
||||
import logging.handlers
|
||||
import vlc
|
||||
import os
|
||||
import re
|
||||
@@ -12,7 +13,6 @@ import sys
|
||||
import json
|
||||
import queue
|
||||
import psutil
|
||||
import logging
|
||||
import requests
|
||||
import time
|
||||
import threading
|
||||
@@ -44,6 +44,31 @@ sio = socketio.Client(
|
||||
)
|
||||
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:
|
||||
def __init__(self):
|
||||
@@ -167,7 +192,7 @@ class IdleScreenManager:
|
||||
self.root.after(IDLE_SCREEN_REFRESH_INTERVAL_MS, self._refresh_recent_data)
|
||||
self.root.mainloop()
|
||||
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
|
||||
|
||||
def _process_commands(self):
|
||||
@@ -358,7 +383,7 @@ def fetch_channel_logo_photo(item):
|
||||
try:
|
||||
photo_image = ImageTk.PhotoImage(background)
|
||||
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}"
|
||||
|
||||
logo_cache[cache_key] = photo_image
|
||||
@@ -482,7 +507,7 @@ def sync_recent_channels_from_server():
|
||||
if normalized_items:
|
||||
return save_recent_channels_to_db(normalized_items)
|
||||
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
|
||||
|
||||
@@ -494,7 +519,34 @@ def focus_vlc_player():
|
||||
try:
|
||||
media_principal.set_fullscreen(True)
|
||||
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():
|
||||
@@ -507,11 +559,11 @@ def wait_for_vlc_playback_start():
|
||||
focus_vlc_player()
|
||||
return True
|
||||
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
|
||||
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
|
||||
|
||||
|
||||
@@ -617,6 +669,7 @@ def new_media_player(url):
|
||||
media.add_option(f":vout={forced_vout}")
|
||||
|
||||
player.set_media(media)
|
||||
player.event_manager().event_attach(vlc.EventType.MediaPlayerEncounteredError, on_vlc_encountered_error)
|
||||
return player
|
||||
|
||||
|
||||
@@ -650,7 +703,7 @@ def capture_and_upload_snapshot():
|
||||
snapshot_path = get_snapshot_filepath()
|
||||
snapshot_status = media_principal.video_take_snapshot(0, snapshot_path, 0, 0)
|
||||
if snapshot_status != 0:
|
||||
print('snapshot capture failed', flush=True)
|
||||
logger.error("Snapshot capture failed")
|
||||
return False
|
||||
|
||||
upload_snapshot_file(snapshot_path)
|
||||
@@ -663,7 +716,7 @@ def snapshot_loop():
|
||||
if auto_snapshot_enabled and data_running:
|
||||
capture_and_upload_snapshot()
|
||||
except Exception as exc:
|
||||
print('snapshot loop error', exc, flush=True)
|
||||
logger.warning("Snapshot loop error: %s", exc)
|
||||
time.sleep(SNAPSHOT_INTERVAL_SECONDS)
|
||||
|
||||
def RepresentsInt(s):
|
||||
@@ -677,7 +730,7 @@ def convertMillis(millis):
|
||||
seconds=(millis/1000)%60
|
||||
minutes=(millis/(1000*60))%60
|
||||
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
|
||||
|
||||
def is_vpn_connected():
|
||||
@@ -730,12 +783,12 @@ def findInfo(tag, s):
|
||||
|
||||
def restart_process():
|
||||
try:
|
||||
print('restart new process')
|
||||
logger.info("Restarting process")
|
||||
p = psutil.Process(os.getpid())
|
||||
for handler in p.get_open_files() + p.connections():
|
||||
os.close(handler.fd)
|
||||
except Exception as e:
|
||||
logging.error(e)
|
||||
logger.error(e)
|
||||
|
||||
python = sys.executable
|
||||
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.Position, 8)
|
||||
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):
|
||||
@@ -781,20 +834,20 @@ def playMedia(id_channel, url_channel, name_channel, id_playlist):
|
||||
media_principal.stop()
|
||||
media_principal.release()
|
||||
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)
|
||||
try:
|
||||
media_principal.set_fullscreen(True)
|
||||
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.play()
|
||||
start_playback_transition()
|
||||
mediaRunning = name_channel
|
||||
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)
|
||||
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_string(vlc.VideoMarqueeOption.Text, name_channel)
|
||||
@@ -813,7 +866,7 @@ def update_source():
|
||||
updated_file.write(r.content)
|
||||
os.replace(temp_path, path + 'IPTV.py')
|
||||
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):
|
||||
os.remove(temp_path)
|
||||
raise
|
||||
@@ -821,14 +874,14 @@ def update_source():
|
||||
|
||||
def wait_for_internet_connection():
|
||||
while True:
|
||||
print("Checking connexion ...", flush=True)
|
||||
logger.info("Checking connexion ...")
|
||||
for check_url, verify_ssl in CONNECTIVITY_CHECK_TARGETS:
|
||||
try:
|
||||
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
|
||||
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)
|
||||
|
||||
@@ -856,18 +909,18 @@ def ensure_socket_connection(force_reset=False):
|
||||
pass
|
||||
|
||||
wait_for_internet_connection()
|
||||
print('Attempting socket connection...', flush=True)
|
||||
logger.info("Attempting socket connection...")
|
||||
sio.connect(
|
||||
url_update_server,
|
||||
wait=True,
|
||||
wait_timeout=SOCKET_CONNECT_TIMEOUT,
|
||||
transports=['websocket', 'polling']
|
||||
)
|
||||
print('Socket connection ready', flush=True)
|
||||
logger.info("Socket connection ready")
|
||||
server_connection_ready = True
|
||||
return True
|
||||
except Exception as exc:
|
||||
print('Socket reconnection failed', exc, flush=True)
|
||||
logger.warning("Socket reconnection failed: %s", exc)
|
||||
server_connection_ready = False
|
||||
return False
|
||||
|
||||
@@ -876,7 +929,7 @@ def polling_server():
|
||||
global media_principal
|
||||
global data_running
|
||||
global status_vpn
|
||||
print('polling_server')
|
||||
logger.info("Polling loop started")
|
||||
while True:
|
||||
try:
|
||||
if not sio.connected:
|
||||
@@ -890,7 +943,7 @@ def polling_server():
|
||||
percentage = media_principal.get_position()*100
|
||||
|
||||
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}
|
||||
sio.emit('polling', {
|
||||
'name': name_device,
|
||||
@@ -902,7 +955,7 @@ def polling_server():
|
||||
'snapshot_interval_seconds': SNAPSHOT_INTERVAL_SECONDS
|
||||
})
|
||||
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)
|
||||
time.sleep(POLLING_INTERVAL_SECONDS)
|
||||
|
||||
@@ -911,23 +964,23 @@ def polling_server():
|
||||
|
||||
def sendMessage(fromSidDevice, toSidDevice, typeCommand, payload, 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():
|
||||
sio.emit('communication', new_data)
|
||||
else:
|
||||
print('Socket disconnected, unable to send message', flush=True)
|
||||
logger.warning("Socket disconnected, unable to send message")
|
||||
|
||||
|
||||
@sio.event
|
||||
def connect():
|
||||
global server_connection_ready
|
||||
server_connection_ready = True
|
||||
print('connection established')
|
||||
logger.info("Connection established")
|
||||
|
||||
|
||||
@sio.event
|
||||
def connect_error(data):
|
||||
print('connection error', data, flush=True)
|
||||
logger.error("Connection error: %s", data)
|
||||
|
||||
|
||||
@sio.event
|
||||
@@ -973,14 +1026,14 @@ def communication(data):
|
||||
# PLAY A CHANNEL ON
|
||||
# --------------------------------
|
||||
if typeCommand == "on":
|
||||
print ("switch on")
|
||||
logger.info("Switch on requested")
|
||||
data_channel = data['payload']
|
||||
name_channel = data_channel['name']
|
||||
name_channel = data_channel['name']
|
||||
url_channel = data_channel['url']
|
||||
id_channel = data_channel['id_channel']
|
||||
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)
|
||||
sendMessage(toSidDevice, fromSidDevice, 'playing', name_channel, 0)
|
||||
sendMessage(toSidDevice, fromSidDevice, 'warning', 'Starting media', 0)
|
||||
@@ -1105,7 +1158,7 @@ def communication(data):
|
||||
if typeCommand == "changeDeviceName":
|
||||
global name_device
|
||||
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'"
|
||||
cur_ = con.cursor()
|
||||
cur_.execute(query)
|
||||
@@ -1134,7 +1187,7 @@ def communication(data):
|
||||
# --------------------------------
|
||||
if typeCommand == "takeSnapshot":
|
||||
try:
|
||||
print('take snapshot')
|
||||
logger.info("Take snapshot requested")
|
||||
time.sleep(1)
|
||||
capture_and_upload_snapshot()
|
||||
except Exception as e:
|
||||
@@ -1152,22 +1205,19 @@ def communication(data):
|
||||
# Command line
|
||||
# --------------------------------
|
||||
if typeCommand == "command_line":
|
||||
logger.info("Command line requested: %s", data['payload'])
|
||||
records = re.split(r' ', data['payload'])
|
||||
#result = subprocess.run(records, capture_output=True, text=True)
|
||||
process = subprocess.Popen(records, stdout=subprocess.PIPE, text=True)
|
||||
process = subprocess.Popen(records, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
|
||||
for line in process.stdout:
|
||||
print(line, end="")
|
||||
logger.info("command_line output: %s", line.rstrip())
|
||||
sendMessage(toSidDevice, fromSidDevice, 'display_command_result', line, 0)
|
||||
|
||||
|
||||
#print(result.stdout)
|
||||
|
||||
|
||||
@sio.event
|
||||
def disconnect():
|
||||
global server_connection_ready
|
||||
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.execute('SELECT SQLITE_VERSION()')
|
||||
data = cur.fetchone()
|
||||
print ("Connexion succeed : SQLite version: %s" % data)
|
||||
logger.info("Connexion succeed : SQLite version: %s", data)
|
||||
cur.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS recent_channels_cache (
|
||||
@@ -1197,7 +1247,7 @@ def initDB():
|
||||
con.commit()
|
||||
|
||||
except (lite.Error, e):
|
||||
print ("Error %s:" % e.args[0])
|
||||
logger.error("Error %s:", e.args[0])
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@@ -1217,9 +1267,9 @@ def initIPTV():
|
||||
record = cur_.fetchone()
|
||||
if record:
|
||||
IdDevice = record[0]
|
||||
print(IdDevice)
|
||||
logger.info("Device ID: %s", IdDevice)
|
||||
else:
|
||||
print("No ID device found")
|
||||
logger.info("No ID device found, generating one")
|
||||
IdDevice = str(uuid.uuid4())
|
||||
query = "INSERT INTO settings (nameParameter, valueParameter) VALUES ('IdDevice', '"+IdDevice+"')"
|
||||
cur_ = con.cursor()
|
||||
@@ -1234,7 +1284,7 @@ def initIPTV():
|
||||
valueParameter = cur_.fetchone()
|
||||
if valueParameter is not None:
|
||||
name_device = valueParameter
|
||||
print('Update device name : ' + str(name_device))
|
||||
logger.info("Update device name : %s", name_device)
|
||||
|
||||
|
||||
# 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__))+'/'
|
||||
con = None
|
||||
pid = os.getppid()
|
||||
print('PID : ', pid)
|
||||
logger.info("PID : %s", pid)
|
||||
|
||||
channels = []
|
||||
playlists = []
|
||||
@@ -1317,15 +1367,11 @@ auth_file = "/home/pi/Documents/auth.txt"
|
||||
|
||||
|
||||
# ----------------------------------------------------------------
|
||||
# ----------------------------------------------------------------
|
||||
|
||||
logging.basicConfig(
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
# ---------------------------------------------------------------
|
||||
# (logging configuré plus haut dans le fichier, juste après les imports)
|
||||
|
||||
# ----------------------------------------------------------------
|
||||
# LOOP PRINCIPAL
|
||||
# LOOP PRINCIPAL
|
||||
# ----------------------------------------------------------------
|
||||
if __name__ == '__main__':
|
||||
|
||||
|
||||
+112
-4
@@ -961,6 +961,8 @@
|
||||
<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="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="getIP()" class="dropdown-item" href="#">Get IP</a></li>
|
||||
<li class="dropdown-submenu">
|
||||
@@ -1152,19 +1154,35 @@
|
||||
|
||||
<!-- ZONE MODAL TERMINAL -->
|
||||
<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-header">
|
||||
<h5 class="modal-title">Terminal </h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="input-group mb-3">
|
||||
<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">
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary">Send command</button>
|
||||
<div class="alert alert-secondary" role="alert" id="txt_result_command">
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary" id="btn_send_terminal_command">Send command</button>
|
||||
<button type="button" class="btn btn-outline-secondary" id="btn_view_device_logs">View streaming logs</button>
|
||||
<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>
|
||||
@@ -1511,6 +1529,7 @@
|
||||
var playlistImportProgress = 0
|
||||
var playlistImportInFlight = false
|
||||
var selectedDeviceSnapshotIntervalSeconds = 10
|
||||
var currentSelectedDeviceData = null
|
||||
|
||||
var socket = io('https://iptv.mrk.ovh', {
|
||||
secure: true,
|
||||
@@ -1549,6 +1568,13 @@
|
||||
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
|
||||
if (data['typeCommand'] == 'progress_analysis') {
|
||||
$('#progress-container').show()
|
||||
@@ -2661,6 +2687,7 @@
|
||||
if (isCurrentDevice) {
|
||||
currentDeviceUpdated = true;
|
||||
selectedDeviceSnapshotData = dataJSON[i];
|
||||
currentSelectedDeviceData = dataJSON[i];
|
||||
|
||||
$('#sidDevice').val( dataJSON[i].sid)
|
||||
var data_running = dataJSON[i].data_running || {}
|
||||
@@ -2684,6 +2711,7 @@
|
||||
|
||||
}
|
||||
if (!currentDeviceUpdated) {
|
||||
currentSelectedDeviceData = null;
|
||||
updateCurrentDeviceStream({label: 'No data', className: 'bg-secondary'}, false);
|
||||
updateSelectedDeviceSnapshot(null, false);
|
||||
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 ---
|
||||
function openAiSearch() {
|
||||
$('#aiSearchAlert').html('');
|
||||
|
||||
Reference in New Issue
Block a user