Panel navigation during real time updates

Hi All,

I’ve created multiple real time dashboards in panel and each one works individually exactly as designed. The dashboards display real time data including pitch, roll, yaw, alt, lat, long, etc. using the full range of Holoviz/Panel indicators and widgets.

When I try and run them together using --glob navigation between apps breaks the background process and it shuts down.

Closed a session running at the / endpoint

I’ve previously created a topic 7346 and I’ve taken this example and updated it for my latest issue. Here a quick and dirty MRE.

app.py

from asyncio import sleep

import holoviews as hv
import pandas as pd
import panel as pn
from holoviews.streams import Buffer
import hvplot.pandas  # no qa

pn.extension()

data = pn.state.cache["data"]

dfstream = Buffer(pd.DataFrame(data, index=[pd.Timestamp.now()]), length=100, index=False)


def plot(data, window_seconds, alpha):
    data = data.rolling(f"{window_seconds}s").mean()
    return data.hvplot(y="y", ylim=(0, 50), alpha=alpha, color="blue", line_width=5)


window_seconds = pn.widgets.IntSlider(value=5, start=1, end=10, name="Window (secs)")
alpha = pn.widgets.FloatSlider(value=1, start=0, end=1, name="Alpha")
iplot = hv.DynamicMap(
    plot,
    streams={
        "data": dfstream.param.data,
        "window_seconds": window_seconds.param.value,
        "alpha": alpha.param.value,
    },
)

pages = {
    "Page 1": pn.Column(iplot, window_seconds, alpha),
    "Page 2": pn.Column("# Page 2", "...more bla"),
}


def show(page):
    return pages[page]


starting_page = pn.state.session_args.get("page", [b"Page 1"])[0].decode()
page = pn.widgets.RadioButtonGroup(
    value=starting_page,
    options=list(pages.keys()),
    name="Page",
    sizing_mode="fixed",
    button_type="success",
)
ishow = pn.bind(show, page=page)
pn.state.location.sync(page, {"value": "page"})

ACCENT_COLOR = "#0072B5"
DEFAULT_PARAMS = {
    "site": "Panel Multi Page App",
    "accent_base_color": ACCENT_COLOR,
    "header_background": ACCENT_COLOR,
}
pn.template.FastListTemplate(
    title="As Single Page App",
    sidebar=[page],
    main=[ishow],
    **DEFAULT_PARAMS,
).servable()


async def run():
    while True:
        await sleep(0.1)
        data = pn.state.cache["data"]
        dfstream.send(pd.DataFrame(data, index=[pd.Timestamp.now()]))


pn.state.onload(run)

app2.py

from asyncio import sleep

import holoviews as hv
import pandas as pd
import panel as pn
from holoviews.streams import Buffer
import hvplot.pandas  # no qa

pn.extension()

data = pn.state.cache["data"]

dfstream = Buffer(pd.DataFrame(data, index=[pd.Timestamp.now()]), length=100, index=False)


def plot(data, window_seconds, alpha):
    data = data.rolling(f"{window_seconds}s").mean()
    return data.hvplot(y="a", ylim=(0, 50), alpha=alpha, color="blue", line_width=5)


window_seconds = pn.widgets.IntSlider(value=5, start=1, end=10, name="Window (secs)")
alpha = pn.widgets.FloatSlider(value=1, start=0, end=1, name="Alpha")
iplot = hv.DynamicMap(
    plot,
    streams={
        "data": dfstream.param.data,
        "window_seconds": window_seconds.param.value,
        "alpha": alpha.param.value,
    },
)

pages = {
    "Page 1": pn.Column(iplot, window_seconds, alpha),
    "Page 2": pn.Column("# Page 2", "...more bla"),
}


def show(page):
    return pages[page]


starting_page = pn.state.session_args.get("page", [b"Page 1"])[0].decode()
page = pn.widgets.RadioButtonGroup(
    value=starting_page,
    options=list(pages.keys()),
    name="Page",
    sizing_mode="fixed",
    button_type="success",
)
ishow = pn.bind(show, page=page)
pn.state.location.sync(page, {"value": "page"})

ACCENT_COLOR = "#0072B5"
DEFAULT_PARAMS = {
    "site": "Panel Multi Page App",
    "accent_base_color": ACCENT_COLOR,
    "header_background": ACCENT_COLOR,
}
pn.template.FastListTemplate(
    title="As Single Page App",
    sidebar=[page],
    main=[ishow],
    **DEFAULT_PARAMS,
).servable()


async def run():
    while True:
        await sleep(0.1)
        data = pn.state.cache["data"]
        dfstream.send(pd.DataFrame(data, index=[pd.Timestamp.now()]))


pn.state.onload(run)

index.py

"""
Panel App Gallery

A Panel-based index page that serves as a gallery of Panel applications with search and pagination functionality.
This demonstrates how to use `panel serve *.py --index index.py` to create a custom Python-based index page
instead of relying on HTML/Jinja templates.

Features:

- Real-time search functionality that filters apps by name and description
- Responsive thumbnails and app metadata display
- Pagination support when there are more than APPS_PER_PAGE results
- Modern Design styling with hover effects
- Auto-discovery of Panel apps in the current directory

Usage:
    panel serve *.py --index index.py --dev --show

This app provides an interactive way to browse and search through a collection of Panel applications,
making it easy to find specific apps in larger collections. Each app displays its name, description,
and thumbnail image with direct links to launch the application.
"""

import math
from typing import List

import panel as pn
import param

# Configuration
APPS_PER_PAGE = 20
ACCENT_COLOR = "#4099da"
ACCENT_FILL_HOVER = "#0072b5"

# Initialize Panel with required extensions
pn.extension("tabulator", sizing_mode="stretch_width")


class AppInfo:
    """Data class to hold information about a Panel app"""

    def __init__(self, name: str, filename: str, description: str = "", thumbnail: str = ""):
        self.name = name
        self.filename = filename
        self.description = description or f"Panel application: {name}"
        self.thumbnail = thumbnail or self._get_default_thumbnail()

    def _get_default_thumbnail(self) -> str:
        """Generate a default thumbnail placeholder"""
        return "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE1MCIgdmlld0JveD0iMCAwIDIwMCAxNTAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIyMDAiIGhlaWdodD0iMTUwIiBmaWxsPSIjRjNGNEY2Ii8+CjxwYXRoIGQ9Ik04NyA2NUg5M1Y1OUg4N1Y2NVoiIGZpbGw9IiM5Q0E4QjciLz4KPHA5YXRoIGQ9Ik05NyA2MUgxMTNWODdIOTdWNjFaIiBmaWxsPSIjOUNBOEI3Ii8+CjxwYXRoIGQ9Ik04MyA3NEgxMTdWNzlIODNWNzRaIiBmaWxsPSIjOUNBOEI3Ii8+CjxwYXRoIGQ9Ik04MyA4Mkg5OFY4N0g4M1Y4MloiIGZpbGw9IiM5Q0E4QjciLz4KPC9zdmc+Cg=="

    def url(self) -> str:
        """Generate the URL to launch the app"""
        return f"./{self.filename.replace('.py', '')}"


class AppGallery(pn.viewable.Viewer):
    """
    A Panel-based gallery for browsing and searching Panel applications.

    This component provides:

    - Search functionality with real-time filtering
    - Pagination for large app collections
    - Thumbnail previews and app metadata
    - Direct links to launch applications
    """

    # Parameters for state management
    search_term = param.String(default="", doc="Current search term")
    current_page = param.Integer(default=1, bounds=(1, None), doc="Current page number")
    apps_per_page = param.Integer(
        default=APPS_PER_PAGE, bounds=(1, 50), doc="Number of apps per page"
    )

    def __init__(self, **params):
        super().__init__(**params)

        # Discover available Panel apps
        self.all_apps = self._discover_apps()

        # Create UI components
        self._create_ui()

    def _discover_apps(self) -> List[AppInfo]:
        """Discover Panel apps in the current directory"""
        apps = []

        # Add some demo apps for illustration
        demo_apps = [
            AppInfo(
                "App",
                "app.py",
                "Interactive dashboard for...",
                thumbnail=r".\assets\images\thumbnails\app.png",
            ),
            AppInfo(
                "App 2",
                "app2.py",
                "Interactive dashboard for...",
                thumbnail=r".\assets\images\thumbnails\app2.png",
            ),
        ]

        apps.extend(demo_apps)

        return sorted(apps, key=lambda x: x.name)

    def _create_ui(self):
        """Create the user interface components"""
        with pn.config.set(sizing_mode="stretch_width"):
            # Header
            self._header = pn.pane.HTML("""
            <div style="text-align: center; padding: 20px 0;">
                <h1 style="color: #2E86AB; margin-bottom: 10px;">Holoviz</h1>
                <h1 style="color: #2E86AB; margin-bottom: 5px;">Real Time Data Analytics</h1>
                <p style="color: #666; font-size: 18px;">Discover and explore applications</p>
            </div>
            """)

            # Search bar
            self._search_input = pn.widgets.TextInput.from_param(
                self.param.search_term,
                placeholder="Search apps by name or description...",
                margin=(10, 20),
                sizing_mode="stretch_width",
            )

            # Apps grid container
            self._apps_container = pn.FlexBox(
                sizing_mode="stretch_width",
                align="center",
            )

            # Pagination controls
            self._pagination = pn.Row(sizing_mode="stretch_width", styles={"margin": "0 auto"})

            # Main layout
            self._layout = pn.Column(
                self._header,
                self._search_input,
                self._apps_container,
                pn.Spacer(sizing_mode="stretch_height"),
                self._pagination,
                max_width=975,
                styles={"margin": "0 auto"},
                align="center",
                sizing_mode="stretch_both",
            )

        # Initial update will be triggered automatically by parameter reactivity
        self._update_display()

    @param.depends("search_term", watch=True)
    def _on_search_change(self):
        """Handle search term changes by resetting to first page"""
        self.current_page = 1

    def _get_filtered_apps(self) -> List[AppInfo]:
        """Get filtered apps based on search term"""
        # Filter apps based on search term
        search_term = str(self.search_term or "")
        if search_term:
            filtered_apps = [
                app
                for app in self.all_apps
                if (
                    search_term.lower() in app.name.lower()
                    or search_term.lower() in app.description.lower()
                )
            ]
        else:
            filtered_apps = self.all_apps

        return filtered_apps

    def _get_paginated_apps(self, apps: List[AppInfo]) -> List[AppInfo]:
        """Get apps for current page"""
        # Safely get parameter values with defaults
        current_page = max(1, getattr(self, "current_page", 1) or 1)
        apps_per_page = max(1, getattr(self, "apps_per_page", APPS_PER_PAGE) or APPS_PER_PAGE)

        start_idx = (current_page - 1) * apps_per_page
        end_idx = start_idx + apps_per_page
        return apps[start_idx:end_idx]

    def _create_app_card(self, app: AppInfo) -> pn.pane.HTML:
        """Create a card for a single app"""
        card_html = f"""
        <div style="
            border: 1px solid var(--neutral-stroke-rest);
            border-radius: 12px;
            padding: 20px;
            margin: 10px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
            transition: all 0.3s ease;
            height: 320px;
            display: flex;
            flex-direction: column;
        " onmouseover="this.style.transform='translateY(-5px)'; this.style.boxShadow='0 8px 25px rgba(0,0,0,0.15)'"
           onmouseout="this.style.transform='translateY(0px)'; this.style.boxShadow='0 2px 4px rgba(0,0,0,0.1)'">
            
            <div style="text-align: center; margin-bottom: 15px;">
                <img src="{app.thumbnail}" 
                     style="width: 100%; height: 120px; object-fit: cover; border-radius: 8px; background: #f5f5f5;" 
                     alt="App thumbnail"/>
            </div>
            
            <h3 style="color: {ACCENT_COLOR}; margin: 0 0 10px 0; font-size: 18px; text-align: center;">
                {app.name}
            </h3>
            
            <p style="color: #666; margin: 0 0 15px 0; font-size: 14px; line-height: 1.4; flex-grow: 1; overflow: hidden;">
                {app.description[:120]}{"..." if len(app.description) > 120 else ""}
            </p>
            
            <div style="text-align: center; margin-top: auto;">
                <a href="{app.url()}" 
                   style="
                       background: {ACCENT_COLOR};
                       color: white;
                       padding: 8px 16px;
                       text-decoration: none;
                       border-radius: 6px;
                       font-weight: 500;
                       transition: background-color 0.2s;
                   "
                   onmouseover="this.style.backgroundColor='{ACCENT_FILL_HOVER}'"
                   onmouseout="this.style.backgroundColor='{ACCENT_COLOR}'">
                    Open App →
                </a>
            </div>
        </div>
        """
        return pn.pane.HTML(card_html, width=300, height=320)

    def _create_pagination_controls(self, total_apps: int) -> pn.Row:
        """Create pagination controls"""
        # Safely get parameter values with defaults
        current_page = max(1, getattr(self, "current_page", 1) or 1)
        apps_per_page = max(1, getattr(self, "apps_per_page", APPS_PER_PAGE) or APPS_PER_PAGE)
        total_pages = math.ceil(total_apps / apps_per_page)

        if total_pages <= 1:
            return pn.Row()

        controls = []

        # Previous button
        if current_page > 1:
            prev_btn = pn.widgets.Button(
                name="← Previous",
                button_style="outline",
            )
            prev_btn.on_click(lambda event: self._go_to_page(current_page - 1))
            controls.append(prev_btn)

        # Page numbers
        start_page = max(1, current_page - 2)
        end_page = min(total_pages, current_page + 2)

        for page_num in range(start_page, end_page + 1):
            if page_num == current_page:
                # Current page (highlighted)
                page_btn = pn.widgets.Button(
                    name=str(page_num), button_type="primary", margin=(5, 5), width=40
                )
            else:
                # Other pages
                page_btn = pn.widgets.Button(
                    name=str(page_num), button_style="outline", margin=(5, 5), width=40
                )
                page_btn.on_click(lambda event, p=page_num: self._go_to_page(p))

            controls.append(page_btn)

        # Next button
        if current_page < total_pages:
            next_btn = pn.widgets.Button(name="Next →", button_style="outline", margin=(5, 10))
            next_btn.on_click(lambda event: self._go_to_page(current_page + 1))
            controls.append(next_btn)

        return pn.Row(*controls)

    def _go_to_page(self, page_num: int):
        """Navigate to a specific page"""
        self.current_page = page_num  # Reactivity will handle the update automatically

    @param.depends("search_term", "current_page", watch=True)
    def _update_display(self):
        """Update the apps display based on current filters and page"""
        # Get filtered apps
        filtered_apps = self._get_filtered_apps()

        # Get paginated apps
        paginated_apps = self._get_paginated_apps(filtered_apps)

        # Clear current display
        self._apps_container.clear()
        self._pagination.clear()

        if not paginated_apps:
            # No apps found
            no_apps_msg = pn.pane.HTML(
                "<div style='text-align: center; padding: 40px; color: #666;'>"
                "<h3>No apps found</h3>"
                "<p>Try adjusting your search terms.</p>"
                "</div>",
                sizing_mode="stretch_width",
            )
            self._apps_container.append(no_apps_msg)
        else:
            # Create grid of app cards
            self._apps_container[:] = [self._create_app_card(app) for app in paginated_apps]

        # Update pagination
        pagination_controls = self._create_pagination_controls(len(filtered_apps))
        if len(pagination_controls):
            self._pagination[:] = pagination_controls

    def __panel__(self):
        """Return the panel for notebook display"""
        return self._layout

    @classmethod
    def create_app(cls, **params):
        """Create a servable app instance"""
        instance = cls(**params)

        return pn.template.FastListTemplate(
            title="Office Hours",
            sidebar_width=0,  # No sidebar needed
            header_background=ACCENT_COLOR,
            main_layout=None,
            main=instance,
            favicon=r"assets\favicon-01.png",
        )


# Serve the app
if __name__ == "__main__":
    # Run with: python index.py
    AppGallery.create_app().show(
        port=5030, autoreload=True, open=True, static_dirs={"assets": "./assets"}
    )
elif pn.state.served:
    # Run with: panel serve *.py --index index.py --dev --show
    AppGallery.create_app().servable()

/tasks/setup.py

import sys
import threading
import time

import numpy as np
import panel as pn

running = True

import random


def random_dict():
    keys = ["x", "y", "z", "a", "b", "c", "d", "e", "f", "g"]
    # Generate a list of random integers corresponding to the number of keys
    random_values = [random.randint(1, 50) for _ in keys]

    return dict(zip(keys, random_values))


def worker():
    """
    Worker function to read data from network object and update the data in the Panel state cache.

    Parameters:
    None

    Returns:
    None
    """

    while running:
        time.sleep(0.001)
        pn.state.cache["data"] = random_dict()


def session_created(session_context):
    """
    Function to handle the creation of a new session.

    Parameters:
    - session_context (object): The context of the session being created.

    Returns:
    None
    """
    print(f"Created a session running at the {session_context.request.uri} endpoint")
    thread = threading.Thread(target=worker)
    thread.start()


pn.state.on_session_created(session_created)


def session_destroyed(session_context):
    """
    Stop the object and exit the program when a session is destroyed.

    Parameters:
    - session_context (object): The context of the session that was destroyed.

    Returns:
    None
    """
    print(f"Closed a session running at the {session_context.request.uri} endpoint")
    sys.exit()


pn.state.on_session_destroyed(session_destroyed)

Running the MRE using the following command and navigating between the apps via the index quickly breaks down.

python -m panel serve .\*.py --glob --setup .\tasks\setup.py --index index.py

Any help would be greatly appreciated.