From 594596d4f586c4947fbc1f7772e20015f990471e Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Tue, 19 May 2026 14:37:33 -0500 Subject: [PATCH] Enhancement: update channel card tooltip to use stable 'started_at' timestamp and refactor getStartDate utility --- CHANGELOG.md | 1 + apps/proxy/live_proxy/channel_status.py | 3 ++- .../components/cards/StreamConnectionCard.jsx | 2 +- .../utils/cards/StreamConnectionCardUtils.js | 19 +++---------------- 4 files changed, 7 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0757ccca..c6841390 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -114,6 +114,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **2-second "stream thread did not terminate within timeout" warning on every channel stop.** `_close_socket` in `input/manager.py` was closing the relay pipe read-end (`self.socket`) before killing the ffmpeg process. The stream OS thread blocks in `select()` on that fd; on Linux, closing an fd from another thread while a `select()` is in progress on it does not reliably interrupt the call (POSIX allows this to be undefined). The thread stayed blocked for the full chunk-timeout (5 s), and `stream_thread.join(timeout=2.0)` always expired first. Fixed by killing ffmpeg first: when ffmpeg dies its copy of the relay write-end closes, delivering EOF to `select()` immediately. `self.socket` is closed afterward as cleanup only. - **Duplicate `stop_channel` calls on every channel shutdown.** `StreamGenerator._cleanup` triggered channel shutdown via two parallel paths: `client_manager.remove_client()` (which fires `handle_client_disconnect` on the owning worker, the correct path) and `_schedule_channel_shutdown_if_needed` (which independently spawned a delayed `stop_channel` greenlet). The duplicate call was suppressed by the `_stopping_channels` guard but produced a redundant log entry and unnecessary greenlet on every shutdown. `_schedule_channel_shutdown_if_needed` has been removed; `handle_client_disconnect` is the sole shutdown trigger. - **Concurrent greenlets could re-enter `_close_socket` during `proc.wait()`.** `self.transcode_process` was set to `None` at the end of the `if proc:` block rather than immediately after capturing the reference. Under gevent, `proc.wait(timeout=0.5)` yields the hub, allowing a second greenlet to enter `_close_socket`, find `self.transcode_process` still set, and attempt a second kill+close. `self.transcode_process = None` is now assigned immediately after `proc = self.transcode_process` so concurrent callers see `None` and skip the block. +- **Channel card "started at" tooltip jumping by 1 second on every stats poll.** The Stats page channel card tooltip showed the channel start time by computing `Date.now() - uptime * 1000` on every render. Because `uptime` is a server-side elapsed-seconds value recomputed each response, this reconstruction drifted by up to 1 second per tick. The basic channel info path now emits `started_at` (the raw Unix timestamp from Redis) alongside `uptime`, matching what the detailed stats path already sent. The frontend `getStartDate` helper now accepts the stable `started_at` timestamp directly, so the displayed wall-clock time is fixed from the first poll and never changes. ### Performance diff --git a/apps/proxy/live_proxy/channel_status.py b/apps/proxy/live_proxy/channel_status.py index fd82d4f7..3f75693e 100644 --- a/apps/proxy/live_proxy/channel_status.py +++ b/apps/proxy/live_proxy/channel_status.py @@ -399,7 +399,8 @@ class ChannelStatus: 'owner': metadata.get(ChannelMetadataField.OWNER), 'buffer_index': int(buffer_index_value) if buffer_index_value else 0, 'client_count': client_count, - 'uptime': uptime + 'uptime': uptime, + 'started_at': created_at if created_at > 0 else None, } channel_name = metadata.get(ChannelMetadataField.CHANNEL_NAME) diff --git a/frontend/src/components/cards/StreamConnectionCard.jsx b/frontend/src/components/cards/StreamConnectionCard.jsx index 57df1cfc..30509a86 100644 --- a/frontend/src/components/cards/StreamConnectionCard.jsx +++ b/frontend/src/components/cards/StreamConnectionCard.jsx @@ -615,7 +615,7 @@ const StreamConnectionCard = ({ - +
{toFriendlyDuration(uptime, 'seconds')} diff --git a/frontend/src/utils/cards/StreamConnectionCardUtils.js b/frontend/src/utils/cards/StreamConnectionCardUtils.js index e3c5720e..574c3b2f 100644 --- a/frontend/src/utils/cards/StreamConnectionCardUtils.js +++ b/frontend/src/utils/cards/StreamConnectionCardUtils.js @@ -17,22 +17,9 @@ export const getBufferingSpeedThreshold = (proxySetting) => { return 1.0; // Default fallback }; -export const getStartDate = (uptime) => { - // Get the current date and time - const currentDate = new Date(); - // Calculate the start date by subtracting uptime (in milliseconds) - const startDate = new Date(currentDate.getTime() - uptime * 1000); - // Format the date as a string (you can adjust the format as needed) - return startDate.toLocaleString({ - weekday: 'short', // optional, adds day of the week - year: 'numeric', - month: '2-digit', - day: '2-digit', - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - hour12: true, // 12-hour format with AM/PM - }); +export const getStartDate = (startedAt) => { + if (!startedAt) return 'Unknown'; + return new Date(startedAt * 1000).toLocaleString(); }; export const getM3uAccountsMap = (m3uAccounts) => {