From 8eb24cb94544d6f6df5f89791b7b7a71175c2efd Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Thu, 26 Feb 2026 16:38:09 -0600 Subject: [PATCH 1/8] Bug Fix: The channel store refactor changed to loading only channel IDs instead of full channel objects, leaving `Object.keys(channels).length` always `0` and incorrectly triggering the onboarding state on any empty filter. Fixed by checking `channelIds.length` instead. --- CHANGELOG.md | 4 ++++ frontend/src/components/tables/ChannelsTable.jsx | 10 +++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 336f22fe..8b3ffbb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- Channel table onboarding shown when filter returns zero results: The channel store refactor changed to loading only channel IDs instead of full channel objects, leaving `Object.keys(channels).length` always `0` and incorrectly triggering the onboarding state on any empty filter. Fixed by checking `channelIds.length` instead. + ## [0.20.1] - 2026-02-26 ### Fixed diff --git a/frontend/src/components/tables/ChannelsTable.jsx b/frontend/src/components/tables/ChannelsTable.jsx index 9a65aae0..06fe652a 100644 --- a/frontend/src/components/tables/ChannelsTable.jsx +++ b/frontend/src/components/tables/ChannelsTable.jsx @@ -282,6 +282,7 @@ const ChannelsTable = ({ onReady }) => { // store/channels const channels = useChannelsStore((s) => s.channels); + const channelIds = useChannelsStore((s) => s.channelIds); const profiles = useChannelsStore((s) => s.profiles); const selectedProfileId = useChannelsStore((s) => s.selectedProfileId); const [tablePrefs, setTablePrefs] = useLocalStorage('channel-table-prefs', { @@ -1438,13 +1439,12 @@ const ChannelsTable = ({ onReady }) => { {/* Table or ghost empty state inside Paper */} - {channelsTableLength === 0 && - Object.keys(channels).length === 0 && ( - - )} + {channelsTableLength === 0 && channelIds.length === 0 && ( + + )} - {(channelsTableLength > 0 || Object.keys(channels).length > 0) && ( + {(channelsTableLength > 0 || channelIds.length > 0) && ( Date: Fri, 27 Feb 2026 15:47:09 -0600 Subject: [PATCH 2/8] Bug Fix: Applying any filter that temporarily empties the channel list (e.g. switching directly between two channel groups, or typing a search query that matches nothing) caused the guide to show a blank/empty view with no programs visible. The `VariableSizeList` unmounts when `filteredChannels` becomes empty, destroying its DOM node and resetting `scrollLeft` to 0. On remount the scroll position was never restored because `initialScrollComplete` was still `true`. Fixed by saving the user's current scroll position when the channel list empties mid-transition, then restoring it once new channels have loaded. On first load the guide still scrolls to the current time as before. --- CHANGELOG.md | 1 + frontend/src/pages/Guide.jsx | 52 +++++++++++++++++++++++++++++++----- 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b3ffbb3..074f6beb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Channel table onboarding shown when filter returns zero results: The channel store refactor changed to loading only channel IDs instead of full channel objects, leaving `Object.keys(channels).length` always `0` and incorrectly triggering the onboarding state on any empty filter. Fixed by checking `channelIds.length` instead. +- TV Guide scrolls to position 0 when a filter yields no results: Applying any filter that temporarily empties the channel list (e.g. switching directly between two channel groups, or typing a search query that matches nothing) caused the guide to show a blank/empty view with no programs visible. The `VariableSizeList` unmounts when `filteredChannels` becomes empty, destroying its DOM node and resetting `scrollLeft` to 0. On remount the scroll position was never restored because `initialScrollComplete` was still `true`. Fixed by saving the user's current scroll position when the channel list empties mid-transition, then restoring it once new channels have loaded. On first load the guide still scrolls to the current time as before. ## [0.20.1] - 2026-02-26 diff --git a/frontend/src/pages/Guide.jsx b/frontend/src/pages/Guide.jsx index c59185f4..310ab78f 100644 --- a/frontend/src/pages/Guide.jsx +++ b/frontend/src/pages/Guide.jsx @@ -9,7 +9,7 @@ import React, { } from 'react'; import useChannelsStore from '../store/channels'; import useLogosStore from '../store/logos'; -import useVideoStore from '../store/useVideoStore'; // NEW import +import useVideoStore from '../store/useVideoStore'; import useSettingsStore from '../store/settings'; import { ActionIcon, @@ -232,7 +232,13 @@ export default function TVChannelGuide({ startDate, endDate }) { return () => { cancelled = true; }; - }, [channelGroups, searchQuery, selectedGroupId, selectedProfileId]); + }, [ + allowAllGroups, + channelGroups, + searchQuery, + selectedGroupId, + selectedProfileId, + ]); // Apply filters when search, group, or profile changes const filteredChannels = useMemo(() => { @@ -609,14 +615,46 @@ export default function TVChannelGuide({ startDate, endDate }) { }); }, []); - // Scroll to the nearest half-hour mark ONLY on initial load - useEffect(() => { - if (programs.length > 0 && !initialScrollComplete) { - syncScrollLeft(calculateScrollPosition(now, start)); + // Holds the scroll position to restore after a filter-induced remount. + // null means "use the default scroll-to-now on first load". + const savedScrollLeftRef = useRef(null); + // When channels become empty (filter transition unmounts the list), save the + // current scroll position so we can restore it once new channels arrive. + // Only save if the initial scroll has already happened — otherwise the saved + // position would be 0 (the DOM default) and we'd skip the scroll-to-now. + useEffect(() => { + if (filteredChannels.length === 0) { + if (initialScrollComplete) { + savedScrollLeftRef.current = guideScrollLeftRef.current; + } + setInitialScrollComplete(false); + } + }, [filteredChannels.length, initialScrollComplete]); + + // Scroll on initial load, or restore saved position after a filter transition. + // Guard with guideRef.current — the VariableSizeList outer div is null while + // unmounted, so we must wait until it remounts before calling syncScrollLeft. + useEffect(() => { + if (programs.length > 0 && !initialScrollComplete && guideRef.current) { + if (savedScrollLeftRef.current !== null) { + // Restore where the user was before the filter change + syncScrollLeft(savedScrollLeftRef.current); + savedScrollLeftRef.current = null; + } else { + // Genuine first load — scroll to current time + syncScrollLeft(calculateScrollPosition(now, start)); + } setInitialScrollComplete(true); } - }, [programs, start, now, initialScrollComplete, syncScrollLeft]); + }, [ + programs, + start, + now, + initialScrollComplete, + syncScrollLeft, + filteredChannels.length, + ]); const findChannelByTvgId = useCallback( (tvgId) => matchChannelByTvgId(channelIdByTvgId, channelById, tvgId), From cdef94fde483b1e81b379b4d5cea51baa411d39d Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Fri, 27 Feb 2026 16:28:06 -0600 Subject: [PATCH 3/8] Bug Fix: Stale streams in the streams table had no hover color change, unlike channels with no streams assigned. Converted the inline `backgroundColor` style to a CSS class (`stale-stream-row`) so the `:hover` rule can apply correctly. Applied the same fix to the channel-streams sub-table, where the teal expanded-row background caused the semi-transparent red tint to visually mismatch; the sub-table now uses a pre-blended solid color via `color-mix()` to match the appearance of stale rows in the main streams table. --- CHANGELOG.md | 1 + .../components/tables/ChannelTableStreams.jsx | 11 +++++----- .../src/components/tables/StreamsTable.jsx | 2 +- frontend/src/components/tables/table.css | 20 +++++++++++++++++++ 4 files changed, 27 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 074f6beb..d16e0d52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Stale stream rows missing hover effect: Stale streams in the streams table had no hover color change, unlike channels with no streams assigned. Converted the inline `backgroundColor` style to a CSS class (`stale-stream-row`) so the `:hover` rule can apply correctly. Applied the same fix to the channel-streams sub-table, where the teal expanded-row background caused the semi-transparent red tint to visually mismatch; the sub-table now uses a pre-blended solid color via `color-mix()` to match the appearance of stale rows in the main streams table. - Channel table onboarding shown when filter returns zero results: The channel store refactor changed to loading only channel IDs instead of full channel objects, leaving `Object.keys(channels).length` always `0` and incorrectly triggering the onboarding state on any empty filter. Fixed by checking `channelIds.length` instead. - TV Guide scrolls to position 0 when a filter yields no results: Applying any filter that temporarily empties the channel list (e.g. switching directly between two channel groups, or typing a search query that matches nothing) caused the guide to show a blank/empty view with no programs visible. The `VariableSizeList` unmounts when `filteredChannels` becomes empty, destroying its DOM node and resetting `scrollLeft` to 0. On remount the scroll position was never restored because `initialScrollComplete` was still `true`. Fixed by saving the user's current scroll position when the channel list empties mid-transition, then restoring it once new channels have loaded. On first load the guide still scrolls to the current time as before. diff --git a/frontend/src/components/tables/ChannelTableStreams.jsx b/frontend/src/components/tables/ChannelTableStreams.jsx index 30dabf3d..40771673 100644 --- a/frontend/src/components/tables/ChannelTableStreams.jsx +++ b/frontend/src/components/tables/ChannelTableStreams.jsx @@ -94,7 +94,7 @@ const DraggableRow = ({ row, index }) => { { }} > {row.getVisibleCells().map((cell) => { - const isStale = row.original.is_stale; return ( { ? cell.column.getSize() : undefined, minWidth: 0, - ...(isStale && { - backgroundColor: 'rgba(239, 68, 68, 0.15)', - }), }} > @@ -614,7 +610,10 @@ const ChannelStreams = ({ channel, isExpanded }) => { const rows = table.getRowModel().rows; return ( - + { getRowStyles: (row) => { if (row.original.is_stale) { return { - backgroundColor: 'rgba(239, 68, 68, 0.15)', + className: 'stale-stream-row', }; } return {}; diff --git a/frontend/src/components/tables/table.css b/frontend/src/components/tables/table.css index 88869f57..b957d741 100644 --- a/frontend/src/components/tables/table.css +++ b/frontend/src/components/tables/table.css @@ -120,6 +120,26 @@ html { /* Darker red on hover */ } +/* Style for stale stream rows */ +.stale-stream-row { + background-color: rgba(239, 68, 68, 0.15) !important; +} + +/* Special hover effect for stale stream rows */ +.table-striped .tbody .tr.stale-stream-row:hover { + background-color: rgba(239, 68, 68, 0.3) !important; +} + +/* Override stale color inside channel streams — pre-blend against #27272a since + the teal container background would otherwise tint the transparent red */ +.channel-streams-container .table-striped .tbody .tr.stale-stream-row { + background-color: color-mix(in srgb, rgb(239, 68, 68) 15%, #27272a) !important; +} + +.channel-streams-container .table-striped .tbody .tr.stale-stream-row:hover { + background-color: color-mix(in srgb, rgb(239, 68, 68) 30%, #27272a) !important; +} + /* Prevent text selection when shift key is pressed */ .shift-key-active { cursor: pointer !important; From 83b56a74292facb1758a8fac7f47f2d5c5374777 Mon Sep 17 00:00:00 2001 From: Marcin Olek Date: Fri, 27 Feb 2026 23:30:08 +0100 Subject: [PATCH 4/8] fix: debian_install build optimise --- debian_install.sh | 64 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 50 insertions(+), 14 deletions(-) diff --git a/debian_install.sh b/debian_install.sh index 3452e5c1..3630161d 100755 --- a/debian_install.sh +++ b/debian_install.sh @@ -168,24 +168,51 @@ install_uv() { setup_python_env() { echo ">>> Setting up Python virtual environment with UV..." - # Install UV globally first - install_uv su - "$DISPATCH_USER" < /dev/null; then - curl -LsSf https://astral.sh/uv/install.sh | sh + set -euo pipefail + cd "$APP_DIR" export PATH="\$HOME/.local/bin:\$PATH" -fi -# Create venv and install dependencies using UV -uv venv env --python $PYTHON_BIN -uv sync --python env/bin/python --no-install-project --no-dev + + command -v uv >/dev/null 2>&1 || curl -LsSf https://astral.sh/uv/install.sh | sh + + rm -rf env + $PYTHON_BIN -m venv env + env/bin/python -m ensurepip --upgrade + + export UV_PROJECT_ENVIRONMENT="$APP_DIR/env" + uv sync --no-dev + + env/bin/python -m pip install -q gunicorn EOSU + ln -sf /usr/bin/ffmpeg "$APP_DIR/env/bin/ffmpeg" } + +############################################################################## +# 6.1) Ensure Environment File +############################################################################## + +ensure_env_file() { + echo ">>> Ensuring DJANGO_SECRET_KEY exists in ${APP_DIR}/.env..." + su - "$DISPATCH_USER" <> .env +fi +EOSU +} + + ############################################################################## # 7) Build Frontend ############################################################################## @@ -231,17 +258,21 @@ create_directories() { django_migrate_collectstatic() { echo ">>> Running Django migrations & collectstatic..." su - "$DISPATCH_USER" < Date: Fri, 27 Feb 2026 22:34:04 -0600 Subject: [PATCH 5/8] changelog: debian_install fixes. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d16e0d52..80c459e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Stale stream rows missing hover effect: Stale streams in the streams table had no hover color change, unlike channels with no streams assigned. Converted the inline `backgroundColor` style to a CSS class (`stale-stream-row`) so the `:hover` rule can apply correctly. Applied the same fix to the channel-streams sub-table, where the teal expanded-row background caused the semi-transparent red tint to visually mismatch; the sub-table now uses a pre-blended solid color via `color-mix()` to match the appearance of stale rows in the main streams table. - Channel table onboarding shown when filter returns zero results: The channel store refactor changed to loading only channel IDs instead of full channel objects, leaving `Object.keys(channels).length` always `0` and incorrectly triggering the onboarding state on any empty filter. Fixed by checking `channelIds.length` instead. - TV Guide scrolls to position 0 when a filter yields no results: Applying any filter that temporarily empties the channel list (e.g. switching directly between two channel groups, or typing a search query that matches nothing) caused the guide to show a blank/empty view with no programs visible. The `VariableSizeList` unmounts when `filteredChannels` becomes empty, destroying its DOM node and resetting `scrollLeft` to 0. On remount the scroll position was never restored because `initialScrollComplete` was still `true`. Fixed by saving the user's current scroll position when the channel list empties mid-transition, then restoring it once new channels have loaded. On first load the guide still scrolls to the current time as before. +- `debian_install.sh` regressions after `uv` migration on clean/minimal Debian installs: fixed pip-less venv (`ensurepip`), missing `gunicorn` for the systemd unit, and inconsistent `DJANGO_SECRET_KEY` availability (now persisted to `.env` via `EnvironmentFile`). Docker unaffected. - Thanks [@marcinolek](https://github.com/marcinolek) ## [0.20.1] - 2026-02-26 From 565d335403e3e2853c8460d0a6b52f1d3b0c82ea Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Sun, 1 Mar 2026 16:54:05 -0600 Subject: [PATCH 6/8] Bug Fix: The EPG filter dropdown was showing all EPG sources regardless of whether they had any channels assigned, and the "No EPG" option was never displayed. Fixed by annotating EPGSource records with a `has_channels` flag (via a lightweight `EXISTS` subquery) so only EPG sources actually in use appear as filter options. "No EPG" now appears only when at least one channel has no EPG assigned, determined via a `page_size=1` query against the existing channel filter endpoint. --- CHANGELOG.md | 1 + apps/epg/api_views.py | 11 ++++++++++ apps/epg/serializers.py | 4 +++- frontend/src/api.js | 17 +++++++++++++- .../src/components/tables/ChannelsTable.jsx | 21 +++++++++--------- frontend/src/store/epgs.jsx | 22 +++++++++++++++++-- 6 files changed, 61 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80c459e1..01e76ed9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- EPG filter regression in channel table (introduced in 0.20.0 channel store refactor): The EPG filter dropdown was showing all EPG sources regardless of whether they had any channels assigned, and the "No EPG" option was never displayed. Fixed by annotating EPGSource records with a `has_channels` flag (via a lightweight `EXISTS` subquery) so only EPG sources actually in use appear as filter options. "No EPG" now appears only when at least one channel has no EPG assigned, determined via a `page_size=1` query against the existing channel filter endpoint. - Stale stream rows missing hover effect: Stale streams in the streams table had no hover color change, unlike channels with no streams assigned. Converted the inline `backgroundColor` style to a CSS class (`stale-stream-row`) so the `:hover` rule can apply correctly. Applied the same fix to the channel-streams sub-table, where the teal expanded-row background caused the semi-transparent red tint to visually mismatch; the sub-table now uses a pre-blended solid color via `color-mix()` to match the appearance of stale rows in the main streams table. - Channel table onboarding shown when filter returns zero results: The channel store refactor changed to loading only channel IDs instead of full channel objects, leaving `Object.keys(channels).length` always `0` and incorrectly triggering the onboarding state on any empty filter. Fixed by checking `channelIds.length` instead. - TV Guide scrolls to position 0 when a filter yields no results: Applying any filter that temporarily empties the channel list (e.g. switching directly between two channel groups, or typing a search query that matches nothing) caused the guide to show a blank/empty view with no programs visible. The `VariableSizeList` unmounts when `filteredChannels` becomes empty, destroying its DOM node and resetting `scrollLeft` to 0. On remount the scroll position was never restored because `initialScrollComplete` was still `true`. Fixed by saving the user's current scroll position when the channel list empties mid-transition, then restoring it once new channels have loaded. On first load the guide still scrolls to the current time as before. diff --git a/apps/epg/api_views.py b/apps/epg/api_views.py index dbae6394..5858fa0e 100644 --- a/apps/epg/api_views.py +++ b/apps/epg/api_views.py @@ -42,6 +42,17 @@ class EPGSourceViewSet(viewsets.ModelViewSet): except KeyError: return [Authenticated()] + def get_queryset(self): + from django.db.models import Exists, OuterRef + from apps.channels.models import Channel + return EPGSource.objects.select_related( + "refresh_task__crontab", "refresh_task__interval" + ).annotate( + has_channels=Exists( + Channel.objects.filter(epg_data__epg_source_id=OuterRef('pk')) + ) + ) + def list(self, request, *args, **kwargs): logger.debug("Listing all EPG sources.") return super().list(request, *args, **kwargs) diff --git a/apps/epg/serializers.py b/apps/epg/serializers.py index 07775809..4b4780ea 100644 --- a/apps/epg/serializers.py +++ b/apps/epg/serializers.py @@ -5,6 +5,7 @@ from apps.channels.models import Channel class EPGSourceSerializer(serializers.ModelSerializer): epg_data_count = serializers.SerializerMethodField() + has_channels = serializers.BooleanField(read_only=True, default=False) read_only_fields = ['created_at', 'updated_at'] url = serializers.CharField( required=False, @@ -32,7 +33,8 @@ class EPGSourceSerializer(serializers.ModelSerializer): 'created_at', 'updated_at', 'custom_properties', - 'epg_data_count' + 'epg_data_count', + 'has_channels', ] def get_epg_data_count(self, obj): diff --git a/frontend/src/api.js b/frontend/src/api.js index 7049b7b8..0ead55ad 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -364,6 +364,9 @@ export default class API { .queryChannels(response, API.lastQueryParams); useChannelsTableStore.getState().setAllQueryIds(ids); + // Refresh the EPG stats since channel EPG assignments may have changed + useEPGsStore.getState().refreshEPGStats(); + return response; } catch (e) { // Handle invalid page error by resetting to page 1 and retrying @@ -1314,13 +1317,25 @@ export default class API { static async getEPGs() { try { const response = await request(`${host}/api/epg/sources/`); - return response; } catch (e) { errorNotification('Failed to retrieve EPGs', e); } } + static async getEPGStats() { + try { + // Query channels with no EPG assigned using the existing filter — page_size=1 + // so we only need the `count` field, not any actual channel data. + const response = await request( + `${host}/api/channels/channels/?epg=null&page=1&page_size=1` + ); + return { has_unassigned_epg_channels: (response?.count ?? 0) > 0 }; + } catch (e) { + errorNotification('Failed to retrieve EPG stats', e); + } + } + static async getEPGData() { try { const response = await request(`${host}/api/epg/epgdata/`); diff --git a/frontend/src/components/tables/ChannelsTable.jsx b/frontend/src/components/tables/ChannelsTable.jsx index 06fe652a..66a8eae5 100644 --- a/frontend/src/components/tables/ChannelsTable.jsx +++ b/frontend/src/components/tables/ChannelsTable.jsx @@ -251,6 +251,9 @@ const ChannelsTable = ({ onReady }) => { const tvgsById = useEPGsStore((s) => s.tvgsById); const epgs = useEPGsStore((s) => s.epgs); const tvgsLoaded = useEPGsStore((s) => s.tvgsLoaded); + const hasUnassignedEPGChannels = useEPGsStore( + (s) => s.hasUnassignedEPGChannels + ); // Get channel logos for logo selection const { ensureLogosLoaded } = useChannelLogoSelection(); @@ -381,19 +384,15 @@ const ChannelsTable = ({ onReady }) => { .map((group) => group.name) .sort((a, b) => a.localeCompare(b)); - let hasUnlinkedChannels = false; const epgOptions = Object.values(epgs) + .filter((epg) => epg.is_active && epg.has_channels) .map((epg) => epg.name) - .sort(); - if (hasUnlinkedChannels) { - epgOptions.unshift('No EPG'); - } - // Map for MultiSelect: value 'null' for 'No EPG', label for display - const epgSelectOptions = epgOptions.map((opt) => - opt === 'No EPG' - ? { value: 'null', label: 'No EPG' } - : { value: opt, label: opt } - ); + .sort((a, b) => a.localeCompare(b)); + // Only show 'No EPG' if there are channels without an EPG assigned + const epgSelectOptions = [ + ...(hasUnassignedEPGChannels ? [{ value: 'null', label: 'No EPG' }] : []), + ...epgOptions.map((opt) => ({ value: opt, label: opt })), + ]; const debouncedFilters = useDebounce(filters, 500, () => { setPagination({ ...pagination, diff --git a/frontend/src/store/epgs.jsx b/frontend/src/store/epgs.jsx index d8a54215..80ff8a34 100644 --- a/frontend/src/store/epgs.jsx +++ b/frontend/src/store/epgs.jsx @@ -15,6 +15,7 @@ const useEPGsStore = create((set) => ({ tvgs: [], tvgsById: {}, tvgsLoaded: false, + hasUnassignedEPGChannels: false, isLoading: false, error: null, refreshProgress: {}, @@ -22,12 +23,18 @@ const useEPGsStore = create((set) => ({ fetchEPGs: async () => { set({ isLoading: true, error: null }); try { - const epgs = await api.getEPGs(); + const [sources, stats] = await Promise.all([ + api.getEPGs(), + api.getEPGStats(), + ]); + const hasUnassignedEPGChannels = + stats?.has_unassigned_epg_channels ?? false; set({ - epgs: epgs.reduce((acc, epg) => { + epgs: (sources ?? []).reduce((acc, epg) => { acc[epg.id] = epg; return acc; }, {}), + hasUnassignedEPGChannels, isLoading: false, }); } catch (error) { @@ -36,6 +43,17 @@ const useEPGsStore = create((set) => ({ } }, + refreshEPGStats: async () => { + try { + const stats = await api.getEPGStats(); + set({ + hasUnassignedEPGChannels: stats?.has_unassigned_epg_channels ?? false, + }); + } catch (error) { + console.error('Failed to refresh EPG stats:', error); + } + }, + fetchEPGData: async () => { set({ isLoading: true, error: null }); try { From b098deae767e411cbfb37ff82058b148b7b04b88 Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Mon, 2 Mar 2026 17:20:55 -0600 Subject: [PATCH 7/8] Got rid of extra api call to query if any channels had no EPG assigned. Moved query to an EXISTS lookup during channel query. --- CHANGELOG.md | 2 +- apps/channels/api_views.py | 7 +++++++ frontend/src/api.js | 16 --------------- .../src/components/tables/ChannelsTable.jsx | 2 +- frontend/src/store/channelsTable.jsx | 18 +++++++++-------- frontend/src/store/epgs.jsx | 20 +------------------ 6 files changed, 20 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01e76ed9..d00dbec6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- EPG filter regression in channel table (introduced in 0.20.0 channel store refactor): The EPG filter dropdown was showing all EPG sources regardless of whether they had any channels assigned, and the "No EPG" option was never displayed. Fixed by annotating EPGSource records with a `has_channels` flag (via a lightweight `EXISTS` subquery) so only EPG sources actually in use appear as filter options. "No EPG" now appears only when at least one channel has no EPG assigned, determined via a `page_size=1` query against the existing channel filter endpoint. +- EPG filter regression in channel table (introduced in 0.20.0 channel store refactor): The EPG filter dropdown was showing all EPG sources regardless of whether they had any channels assigned, and the "No EPG" option was never displayed. Fixed by annotating EPGSource records with a `has_channels` flag (via a lightweight `EXISTS` subquery) so only active EPG sources with at least one channel assigned appear as filter options. "No EPG" now appears only when at least one channel globally has no EPG assigned; this is determined by a second `EXISTS` query embedded directly in the paginated channel response (`has_unassigned_epg_channels`), avoiding any additional network requests. - Stale stream rows missing hover effect: Stale streams in the streams table had no hover color change, unlike channels with no streams assigned. Converted the inline `backgroundColor` style to a CSS class (`stale-stream-row`) so the `:hover` rule can apply correctly. Applied the same fix to the channel-streams sub-table, where the teal expanded-row background caused the semi-transparent red tint to visually mismatch; the sub-table now uses a pre-blended solid color via `color-mix()` to match the appearance of stale rows in the main streams table. - Channel table onboarding shown when filter returns zero results: The channel store refactor changed to loading only channel IDs instead of full channel objects, leaving `Object.keys(channels).length` always `0` and incorrectly triggering the onboarding state on any empty filter. Fixed by checking `channelIds.length` instead. - TV Guide scrolls to position 0 when a filter yields no results: Applying any filter that temporarily empties the channel list (e.g. switching directly between two channel groups, or typing a search query that matches nothing) caused the guide to show a blank/empty view with no programs visible. The `VariableSizeList` unmounts when `filteredChannels` becomes empty, destroying its DOM node and resetting `scrollLeft` to 0. On remount the scroll position was never restored because `initialScrollComplete` was still `true`. Fixed by saving the user's current scroll position when the channel list empties mid-transition, then restoring it once new channels have loaded. On first load the guide still scrolls to the current time as before. diff --git a/apps/channels/api_views.py b/apps/channels/api_views.py index d0fb3d97..55192216 100644 --- a/apps/channels/api_views.py +++ b/apps/channels/api_views.py @@ -413,6 +413,13 @@ class ChannelPagination(PageNumberPagination): return super().paginate_queryset(queryset, request, view) + def get_paginated_response(self, data): + from django.db.models import Exists, OuterRef + has_unassigned = Channel.objects.filter(epg_data__isnull=True).exists() + response = super().get_paginated_response(data) + response.data['has_unassigned_epg_channels'] = has_unassigned + return response + class EPGFilter(django_filters.Filter): """ diff --git a/frontend/src/api.js b/frontend/src/api.js index 0ead55ad..10a836ae 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -364,9 +364,6 @@ export default class API { .queryChannels(response, API.lastQueryParams); useChannelsTableStore.getState().setAllQueryIds(ids); - // Refresh the EPG stats since channel EPG assignments may have changed - useEPGsStore.getState().refreshEPGStats(); - return response; } catch (e) { // Handle invalid page error by resetting to page 1 and retrying @@ -1323,19 +1320,6 @@ export default class API { } } - static async getEPGStats() { - try { - // Query channels with no EPG assigned using the existing filter — page_size=1 - // so we only need the `count` field, not any actual channel data. - const response = await request( - `${host}/api/channels/channels/?epg=null&page=1&page_size=1` - ); - return { has_unassigned_epg_channels: (response?.count ?? 0) > 0 }; - } catch (e) { - errorNotification('Failed to retrieve EPG stats', e); - } - } - static async getEPGData() { try { const response = await request(`${host}/api/epg/epgdata/`); diff --git a/frontend/src/components/tables/ChannelsTable.jsx b/frontend/src/components/tables/ChannelsTable.jsx index 66a8eae5..a39cc431 100644 --- a/frontend/src/components/tables/ChannelsTable.jsx +++ b/frontend/src/components/tables/ChannelsTable.jsx @@ -251,7 +251,7 @@ const ChannelsTable = ({ onReady }) => { const tvgsById = useEPGsStore((s) => s.tvgsById); const epgs = useEPGsStore((s) => s.epgs); const tvgsLoaded = useEPGsStore((s) => s.tvgsLoaded); - const hasUnassignedEPGChannels = useEPGsStore( + const hasUnassignedEPGChannels = useChannelsTableStore( (s) => s.hasUnassignedEPGChannels ); diff --git a/frontend/src/store/channelsTable.jsx b/frontend/src/store/channelsTable.jsx index 2062ff41..7c1f05f2 100644 --- a/frontend/src/store/channelsTable.jsx +++ b/frontend/src/store/channelsTable.jsx @@ -4,6 +4,7 @@ const useChannelsTableStore = create((set, get) => ({ channels: [], pageCount: 0, totalCount: 0, + hasUnassignedEPGChannels: false, sorting: [{ id: 'channel_number', desc: false }], pagination: { pageIndex: 0, @@ -14,14 +15,15 @@ const useChannelsTableStore = create((set, get) => ({ allQueryIds: [], isUnlocked: false, - queryChannels: ({ results, count }, params) => { - set((state) => { - return { - channels: results, - totalCount: count, - pageCount: Math.ceil(count / params.get('page_size')), - }; - }); + queryChannels: ({ results, count, has_unassigned_epg_channels }, params) => { + set((state) => ({ + channels: results, + totalCount: count, + pageCount: Math.ceil(count / params.get('page_size')), + ...(has_unassigned_epg_channels !== undefined && { + hasUnassignedEPGChannels: has_unassigned_epg_channels, + }), + })); }, setAllQueryIds: (allQueryIds) => { diff --git a/frontend/src/store/epgs.jsx b/frontend/src/store/epgs.jsx index 80ff8a34..a68ba1b4 100644 --- a/frontend/src/store/epgs.jsx +++ b/frontend/src/store/epgs.jsx @@ -15,7 +15,6 @@ const useEPGsStore = create((set) => ({ tvgs: [], tvgsById: {}, tvgsLoaded: false, - hasUnassignedEPGChannels: false, isLoading: false, error: null, refreshProgress: {}, @@ -23,18 +22,12 @@ const useEPGsStore = create((set) => ({ fetchEPGs: async () => { set({ isLoading: true, error: null }); try { - const [sources, stats] = await Promise.all([ - api.getEPGs(), - api.getEPGStats(), - ]); - const hasUnassignedEPGChannels = - stats?.has_unassigned_epg_channels ?? false; + const sources = await api.getEPGs(); set({ epgs: (sources ?? []).reduce((acc, epg) => { acc[epg.id] = epg; return acc; }, {}), - hasUnassignedEPGChannels, isLoading: false, }); } catch (error) { @@ -43,17 +36,6 @@ const useEPGsStore = create((set) => ({ } }, - refreshEPGStats: async () => { - try { - const stats = await api.getEPGStats(); - set({ - hasUnassignedEPGChannels: stats?.has_unassigned_epg_channels ?? false, - }); - } catch (error) { - console.error('Failed to refresh EPG stats:', error); - } - }, - fetchEPGData: async () => { set({ isLoading: true, error: null }); try { From 7d95e3ceb4b7ae9543bd5e7fc63562db8db5f272 Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Mon, 2 Mar 2026 17:36:40 -0600 Subject: [PATCH 8/8] =?UTF-8?q?Security:=20Updated=20frontend=20npm=20depe?= =?UTF-8?q?ndencies=20to=20resolve=202=20high-severity=20vulnerabilities:?= =?UTF-8?q?=20=20=20-=20Updated=20`minimatch`=20to=20=E2=89=A510.2.3,=20re?= =?UTF-8?q?solving=20**high**=20ReDoS=20via=20matchOne()=20combinatorial?= =?UTF-8?q?=20backtracking=20with=20multiple=20non-adjacent=20GLOBSTAR=20s?= =?UTF-8?q?egments=20([GHSA-7r86-cg39-jmmj](https://github.com/advisories/?= =?UTF-8?q?GHSA-7r86-cg39-jmmj))=20=20=20-=20Updated=20`rollup`=20to=20?= =?UTF-8?q?=E2=89=A54.58.1,=20resolving=20**high**=20Arbitrary=20File=20Wr?= =?UTF-8?q?ite=20via=20Path=20Traversal=20([GHSA-mw96-cpmx-2vgc](https://g?= =?UTF-8?q?ithub.com/advisories/GHSA-mw96-cpmx-2vgc))?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 6 ++ frontend/package-lock.json | 212 ++++++++++++++++++------------------- 2 files changed, 112 insertions(+), 106 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d00dbec6..70b06211 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Security + +- Updated frontend npm dependencies to resolve 2 high-severity vulnerabilities: + - Updated `minimatch` to ≥10.2.3, resolving **high** ReDoS via matchOne() combinatorial backtracking with multiple non-adjacent GLOBSTAR segments ([GHSA-7r86-cg39-jmmj](https://github.com/advisories/GHSA-7r86-cg39-jmmj)) + - Updated `rollup` to ≥4.58.1, resolving **high** Arbitrary File Write via Path Traversal ([GHSA-mw96-cpmx-2vgc](https://github.com/advisories/GHSA-mw96-cpmx-2vgc)) + ### Fixed - EPG filter regression in channel table (introduced in 0.20.0 channel store refactor): The EPG filter dropdown was showing all EPG sources regardless of whether they had any channels assigned, and the "No EPG" option was never displayed. Fixed by annotating EPGSource records with a `has_channels` flag (via a lightweight `EXISTS` subquery) so only active EPG sources with at least one channel assigned appear as filter options. "No EPG" now appears only when at least one channel globally has no EPG assigned; this is determined by a second `EXISTS` query embedded directly in the paginated channel response (`has_unassigned_epg_channels`), avoiding any additional network requests. diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 9b16acc8..82c718eb 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1491,9 +1491,9 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.58.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.58.0.tgz", - "integrity": "sha512-mr0tmS/4FoVk1cnaeN244A/wjvGDNItZKR8hRhnmCzygyRXYtKF5jVDSIILR1U97CTzAYmbgIj/Dukg62ggG5w==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", "cpu": [ "arm" ], @@ -1505,9 +1505,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.58.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.58.0.tgz", - "integrity": "sha512-+s++dbp+/RTte62mQD9wLSbiMTV+xr/PeRJEc/sFZFSBRlHPNPVaf5FXlzAL77Mr8FtSfQqCN+I598M8U41ccQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", "cpu": [ "arm64" ], @@ -1519,9 +1519,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.58.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.58.0.tgz", - "integrity": "sha512-MFWBwTcYs0jZbINQBXHfSrpSQJq3IUOakcKPzfeSznONop14Pxuqa0Kg19GD0rNBMPQI2tFtu3UzapZpH0Uc1Q==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", "cpu": [ "arm64" ], @@ -1533,9 +1533,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.58.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.58.0.tgz", - "integrity": "sha512-yiKJY7pj9c9JwzuKYLFaDZw5gma3fI9bkPEIyofvVfsPqjCWPglSHdpdwXpKGvDeYDms3Qal8qGMEHZ1M/4Udg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", "cpu": [ "x64" ], @@ -1547,9 +1547,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.58.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.58.0.tgz", - "integrity": "sha512-x97kCoBh5MOevpn/CNK9W1x8BEzO238541BGWBc315uOlN0AD/ifZ1msg+ZQB05Ux+VF6EcYqpiagfLJ8U3LvQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", "cpu": [ "arm64" ], @@ -1561,9 +1561,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.58.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.58.0.tgz", - "integrity": "sha512-Aa8jPoZ6IQAG2eIrcXPpjRcMjROMFxCt1UYPZZtCxRV68WkuSigYtQ/7Zwrcr2IvtNJo7T2JfDXyMLxq5L4Jlg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", "cpu": [ "x64" ], @@ -1575,9 +1575,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.58.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.58.0.tgz", - "integrity": "sha512-Ob8YgT5kD/lSIYW2Rcngs5kNB/44Q2RzBSPz9brf2WEtcGR7/f/E9HeHn1wYaAwKBni+bdXEwgHvUd0x12lQSA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", "cpu": [ "arm" ], @@ -1589,9 +1589,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.58.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.58.0.tgz", - "integrity": "sha512-K+RI5oP1ceqoadvNt1FecL17Qtw/n9BgRSzxif3rTL2QlIu88ccvY+Y9nnHe/cmT5zbH9+bpiJuG1mGHRVwF4Q==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", "cpu": [ "arm" ], @@ -1603,9 +1603,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.58.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.58.0.tgz", - "integrity": "sha512-T+17JAsCKUjmbopcKepJjHWHXSjeW7O5PL7lEFaeQmiVyw4kkc5/lyYKzrv6ElWRX/MrEWfPiJWqbTvfIvjM1Q==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", "cpu": [ "arm64" ], @@ -1617,9 +1617,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.58.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.58.0.tgz", - "integrity": "sha512-cCePktb9+6R9itIJdeCFF9txPU7pQeEHB5AbHu/MKsfH/k70ZtOeq1k4YAtBv9Z7mmKI5/wOLYjQ+B9QdxR6LA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", "cpu": [ "arm64" ], @@ -1631,9 +1631,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.58.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.58.0.tgz", - "integrity": "sha512-iekUaLkfliAsDl4/xSdoCJ1gnnIXvoNz85C8U8+ZxknM5pBStfZjeXgB8lXobDQvvPRCN8FPmmuTtH+z95HTmg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", "cpu": [ "loong64" ], @@ -1645,9 +1645,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.58.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.58.0.tgz", - "integrity": "sha512-68ofRgJNl/jYJbxFjCKE7IwhbfxOl1muPN4KbIqAIe32lm22KmU7E8OPvyy68HTNkI2iV/c8y2kSPSm2mW/Q9Q==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", "cpu": [ "loong64" ], @@ -1659,9 +1659,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.58.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.58.0.tgz", - "integrity": "sha512-dpz8vT0i+JqUKuSNPCP5SYyIV2Lh0sNL1+FhM7eLC457d5B9/BC3kDPp5BBftMmTNsBarcPcoz5UGSsnCiw4XQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", "cpu": [ "ppc64" ], @@ -1673,9 +1673,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.58.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.58.0.tgz", - "integrity": "sha512-4gdkkf9UJ7tafnweBCR/mk4jf3Jfl0cKX9Np80t5i78kjIH0ZdezUv/JDI2VtruE5lunfACqftJ8dIMGN4oHew==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", "cpu": [ "ppc64" ], @@ -1687,9 +1687,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.58.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.58.0.tgz", - "integrity": "sha512-YFS4vPnOkDTD/JriUeeZurFYoJhPf9GQQEF/v4lltp3mVcBmnsAdjEWhr2cjUCZzZNzxCG0HZOvJU44UGHSdzw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", "cpu": [ "riscv64" ], @@ -1701,9 +1701,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.58.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.58.0.tgz", - "integrity": "sha512-x2xgZlFne+QVNKV8b4wwaCS8pwq3y14zedZ5DqLzjdRITvreBk//4Knbcvm7+lWmms9V9qFp60MtUd0/t/PXPw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", "cpu": [ "riscv64" ], @@ -1715,9 +1715,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.58.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.58.0.tgz", - "integrity": "sha512-jIhrujyn4UnWF8S+DHSkAkDEO3hLX0cjzxJZPLF80xFyzyUIYgSMRcYQ3+uqEoyDD2beGq7Dj7edi8OnJcS/hg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", "cpu": [ "s390x" ], @@ -1729,9 +1729,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.58.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.58.0.tgz", - "integrity": "sha512-+410Srdoh78MKSJxTQ+hZ/Mx+ajd6RjjPwBPNd0R3J9FtL6ZA0GqiiyNjCO9In0IzZkCNrpGymSfn+kgyPQocg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", "cpu": [ "x64" ], @@ -1743,9 +1743,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.58.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.58.0.tgz", - "integrity": "sha512-ZjMyby5SICi227y1MTR3VYBpFTdZs823Rs/hpakufleBoufoOIB6jtm9FEoxn/cgO7l6PM2rCEl5Kre5vX0QrQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", "cpu": [ "x64" ], @@ -1757,9 +1757,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.58.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.58.0.tgz", - "integrity": "sha512-ds4iwfYkSQ0k1nb8LTcyXw//ToHOnNTJtceySpL3fa7tc/AsE+UpUFphW126A6fKBGJD5dhRvg8zw1rvoGFxmw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", "cpu": [ "x64" ], @@ -1771,9 +1771,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.58.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.58.0.tgz", - "integrity": "sha512-fd/zpJniln4ICdPkjWFhZYeY/bpnaN9pGa6ko+5WD38I0tTqk9lXMgXZg09MNdhpARngmxiCg0B0XUamNw/5BQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", "cpu": [ "arm64" ], @@ -1785,9 +1785,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.58.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.58.0.tgz", - "integrity": "sha512-YpG8dUOip7DCz3nr/JUfPbIUo+2d/dy++5bFzgi4ugOGBIox+qMbbqt/JoORwvI/C9Kn2tz6+Bieoqd5+B1CjA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", "cpu": [ "arm64" ], @@ -1799,9 +1799,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.58.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.58.0.tgz", - "integrity": "sha512-b9DI8jpFQVh4hIXFr0/+N/TzLdpBIoPzjt0Rt4xJbW3mzguV3mduR9cNgiuFcuL/TeORejJhCWiAXe3E/6PxWA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", "cpu": [ "ia32" ], @@ -1813,9 +1813,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.58.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.58.0.tgz", - "integrity": "sha512-CSrVpmoRJFN06LL9xhkitkwUcTZtIotYAF5p6XOR2zW0Zz5mzb3IPpcoPhB02frzMHFNo1reQ9xSF5fFm3hUsQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", "cpu": [ "x64" ], @@ -1827,9 +1827,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.58.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.58.0.tgz", - "integrity": "sha512-QFsBgQNTnh5K0t/sBsjJLq24YVqEIVkGpfN2VHsnN90soZyhaiA9UUHufcctVNL4ypJY0wrwad0wslx2KJQ1/w==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", "cpu": [ "x64" ], @@ -4112,9 +4112,9 @@ } }, "node_modules/minimatch": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.2.tgz", - "integrity": "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -4925,9 +4925,9 @@ } }, "node_modules/rollup": { - "version": "4.58.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.58.0.tgz", - "integrity": "sha512-wbT0mBmWbIvvq8NeEYWWvevvxnOyhKChir47S66WCxw1SXqhw7ssIYejnQEVt7XYQpsj2y8F9PM+Cr3SNEa0gw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "dev": true, "license": "MIT", "dependencies": { @@ -4941,31 +4941,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.58.0", - "@rollup/rollup-android-arm64": "4.58.0", - "@rollup/rollup-darwin-arm64": "4.58.0", - "@rollup/rollup-darwin-x64": "4.58.0", - "@rollup/rollup-freebsd-arm64": "4.58.0", - "@rollup/rollup-freebsd-x64": "4.58.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.58.0", - "@rollup/rollup-linux-arm-musleabihf": "4.58.0", - "@rollup/rollup-linux-arm64-gnu": "4.58.0", - "@rollup/rollup-linux-arm64-musl": "4.58.0", - "@rollup/rollup-linux-loong64-gnu": "4.58.0", - "@rollup/rollup-linux-loong64-musl": "4.58.0", - "@rollup/rollup-linux-ppc64-gnu": "4.58.0", - "@rollup/rollup-linux-ppc64-musl": "4.58.0", - "@rollup/rollup-linux-riscv64-gnu": "4.58.0", - "@rollup/rollup-linux-riscv64-musl": "4.58.0", - "@rollup/rollup-linux-s390x-gnu": "4.58.0", - "@rollup/rollup-linux-x64-gnu": "4.58.0", - "@rollup/rollup-linux-x64-musl": "4.58.0", - "@rollup/rollup-openbsd-x64": "4.58.0", - "@rollup/rollup-openharmony-arm64": "4.58.0", - "@rollup/rollup-win32-arm64-msvc": "4.58.0", - "@rollup/rollup-win32-ia32-msvc": "4.58.0", - "@rollup/rollup-win32-x64-gnu": "4.58.0", - "@rollup/rollup-win32-x64-msvc": "4.58.0", + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" } },