diff --git a/docs/app/reflex_docs/reflex_docs.py b/docs/app/reflex_docs/reflex_docs.py
index 00dd00298bd..b891fe39518 100644
--- a/docs/app/reflex_docs/reflex_docs.py
+++ b/docs/app/reflex_docs/reflex_docs.py
@@ -6,6 +6,7 @@
import reflex as rx
import reflex_enterprise as rxe
from reflex_site_shared import styles
+from reflex_site_shared.backend.status import monitor_checkly_status
from reflex_site_shared.constants import REFLEX_ASSETS_CDN
from reflex_site_shared.meta.meta import favicons_links, to_cdn_image_url
from reflex_site_shared.telemetry import get_pixel_website_trackers
@@ -47,6 +48,8 @@
],
)
+app.register_lifespan_task(monitor_checkly_status)
+
# XXX: The app is TOO BIG to build on Windows, so explicitly disallow it except for testing
if sys.platform == "win32":
if not os.environ.get("REFLEX_WEB_WINDOWS_OVERRIDE"):
diff --git a/docs/app/reflex_docs/templates/docpage/docpage.py b/docs/app/reflex_docs/templates/docpage/docpage.py
index bbd7625108f..b652d867d01 100644
--- a/docs/app/reflex_docs/templates/docpage/docpage.py
+++ b/docs/app/reflex_docs/templates/docpage/docpage.py
@@ -9,12 +9,14 @@
from reflex.components.radix.themes.base import LiteralAccentColor
from reflex.experimental.client_state import ClientStateVar
from reflex.utils.format import to_snake_case, to_title_case
+from reflex_site_shared.backend.status import StatusState
from reflex_site_shared.components.blocks.code import *
from reflex_site_shared.components.blocks.demo import *
from reflex_site_shared.components.blocks.headings import *
from reflex_site_shared.components.blocks.typography import *
from reflex_site_shared.components.icons import get_icon
from reflex_site_shared.components.marketing_button import button as marketing_button
+from reflex_site_shared.components.server_status import server_status
from reflex_site_shared.route import Route, get_path
from reflex_site_shared.styles.colors import c_color
from reflex_site_shared.utils.docpage import right_sidebar_item_highlight
@@ -284,9 +286,13 @@ def docpage_footer(path: str):
menu_socials(),
class_name="flex flex-row gap-6 justify-between items-end w-full",
),
- rx.text(
- f"Copyright © {datetime.now().year} Pynecone, Inc.",
- class_name="font-small text-slate-9",
+ rx.el.div(
+ rx.text(
+ f"Copyright © {datetime.now().year} Pynecone, Inc.",
+ class_name="font-small text-slate-9",
+ ),
+ server_status(StatusState.status),
+ class_name="flex flex-row items-center gap-4 justify-between w-full",
),
class_name="flex flex-col justify-between gap-10 py-6 lg:py-8 w-full",
),
diff --git a/packages/reflex-site-shared/src/reflex_site_shared/backend/status.py b/packages/reflex-site-shared/src/reflex_site_shared/backend/status.py
new file mode 100644
index 00000000000..e4e4caddc2a
--- /dev/null
+++ b/packages/reflex-site-shared/src/reflex_site_shared/backend/status.py
@@ -0,0 +1,111 @@
+"""Checkly-backed service status state and polling utilities."""
+
+import asyncio
+import contextlib
+from enum import StrEnum
+
+import httpx
+
+import reflex as rx
+from reflex_site_shared.constants import (
+ CHECKLY_ACCOUNT_ID,
+ CHECKLY_API_BASE_URL,
+ CHECKLY_API_KEY,
+ CHECKLY_CHECK_GROUP_ID,
+)
+
+
+class ServiceStatus(StrEnum):
+ """Supported service health states exposed in the UI."""
+
+ SUCCESS = "Success"
+ WARNING = "Warning"
+ CRITICAL = "Critical"
+
+
+CURRENT_STATUS = ServiceStatus.SUCCESS.value
+
+
+# Check status of each check in parallel
+async def check_status(check_id: str) -> dict:
+ """Fetch status flags for a single Checkly check.
+
+ Returns:
+ A dictionary with failure and degraded flags.
+ """
+ status_url = f"{CHECKLY_API_BASE_URL}/check-statuses/{check_id}"
+ async with httpx.AsyncClient() as client:
+ status_response = await client.get(
+ status_url,
+ headers={
+ "Authorization": f"Bearer {CHECKLY_API_KEY}",
+ "X-Checkly-Account": CHECKLY_ACCOUNT_ID,
+ },
+ )
+ if status_response.status_code == 200:
+ status_data = status_response.json()
+ return {
+ "has_failures": status_data.get("hasFailures", False),
+ "is_degraded": status_data.get("isDegraded", False),
+ }
+
+ return {"has_failures": False, "is_degraded": False}
+
+
+async def monitor_checkly_status() -> None:
+ """Continuously monitor Checkly check group status.
+
+ Updates the global STATUS variable every 60 seconds.
+ - Critical: if any check has failures
+ - Warning: if no failures but some checks are degraded
+ - Success: all checks are healthy
+
+ """
+ if not all((CHECKLY_API_KEY, CHECKLY_ACCOUNT_ID, CHECKLY_CHECK_GROUP_ID)):
+ return
+
+ headers = {
+ "Authorization": f"Bearer {CHECKLY_API_KEY}",
+ "X-Checkly-Account": CHECKLY_ACCOUNT_ID,
+ }
+
+ try:
+ while True:
+ with contextlib.suppress(Exception):
+ global CURRENT_STATUS
+
+ # Get checks in this group
+ checks_url = f"{CHECKLY_API_BASE_URL}/check-groups/{CHECKLY_CHECK_GROUP_ID}/checks"
+ async with httpx.AsyncClient(timeout=httpx.Timeout(30)) as client:
+ checks_response = await client.get(checks_url, headers=headers)
+ if checks_response.status_code == 200:
+ checks = checks_response.json()
+
+ check_ids = [check.get("id") for check in checks if check.get("id")]
+ results = await asyncio.gather(*[
+ check_status(check_id) for check_id in check_ids
+ ])
+
+ # Determine overall status
+ has_any_failures = any(r["has_failures"] for r in results)
+ has_any_degraded = any(r["is_degraded"] for r in results)
+
+ if has_any_failures:
+ CURRENT_STATUS = ServiceStatus.CRITICAL.value
+ elif has_any_degraded:
+ CURRENT_STATUS = ServiceStatus.WARNING.value
+ else:
+ CURRENT_STATUS = ServiceStatus.SUCCESS.value
+
+ await asyncio.sleep(60)
+ except asyncio.CancelledError:
+ pass
+
+
+class StatusState(rx.State):
+ """Reflex state that exposes the current service status."""
+
+ @rx.var(interval=60)
+ def status(self) -> str:
+ """Return the current status value for the status pill."""
+ return CURRENT_STATUS
diff --git a/packages/reflex-site-shared/src/reflex_site_shared/components/icons.py b/packages/reflex-site-shared/src/reflex_site_shared/components/icons.py
index 66488a21e3a..0c4312787f1 100644
--- a/packages/reflex-site-shared/src/reflex_site_shared/components/icons.py
+++ b/packages/reflex-site-shared/src/reflex_site_shared/components/icons.py
@@ -568,6 +568,10 @@
"""
+
+circle = """
+"""
+
ICONS = {
# Socials
"github": github,
@@ -658,6 +662,7 @@
"moon_footer": moon_footer,
"sun_footer": sun_footer,
"computer_footer": computer_footer,
+ "circle": circle,
}
diff --git a/packages/reflex-site-shared/src/reflex_site_shared/components/server_status.py b/packages/reflex-site-shared/src/reflex_site_shared/components/server_status.py
new file mode 100644
index 00000000000..60f16e44c8c
--- /dev/null
+++ b/packages/reflex-site-shared/src/reflex_site_shared/components/server_status.py
@@ -0,0 +1,88 @@
+"""Server status badge component used in site footers."""
+
+from typing import Literal
+
+import reflex as rx
+from reflex_site_shared.components.icons import get_icon
+from reflex_site_shared.constants import STATUS_WEB_URL
+
+StatusVariant = Literal["Success", "Warning", "Critical"]
+
+DEFAULT_CLASS_NAME = "inline-flex flex-row gap-1.5 items-center font-medium text-sm px-2.5 rounded-[10px] h-9 hover:bg-secondary-3 transition-bg"
+
+STATUS_TEXT_COLORS: dict[StatusVariant, str] = {
+ "Success": "text-success-9",
+ "Warning": "text-warning-11",
+ "Critical": "text-destructive-10",
+}
+
+
+STATUS_VARIANT_TEXT: dict[StatusVariant, str] = {
+ "Success": "All servers are operational",
+ "Warning": "Some servers are unavailable",
+ "Critical": "All servers are down",
+}
+
+STATUS_ICON_COLORS: dict[StatusVariant, str] = {
+ "Success": "!text-success-8",
+ "Warning": "!text-warning-8",
+ "Critical": "!text-destructive-9",
+}
+
+
+def _status_icon(color: str) -> rx.Component:
+ """Create a fresh status icon component for each render branch.
+
+ Returns:
+ A new circle icon component with the given color class.
+ """
+ return get_icon("circle", class_name=color)
+
+
+def server_status(status: StatusVariant | rx.Var[str]) -> rx.Component:
+ """Create a server status component.
+
+ Args:
+ status: The status of the server.
+
+ Returns:
+ A linked status badge that points to the public status page.
+
+ """
+ return rx.el.a(
+ rx.match(
+ status,
+ (
+ "Success",
+ rx.el.div(
+ _status_icon(STATUS_ICON_COLORS["Success"]),
+ STATUS_VARIANT_TEXT["Success"],
+ class_name=f"{DEFAULT_CLASS_NAME} {STATUS_TEXT_COLORS['Success']}",
+ ),
+ ),
+ (
+ "Warning",
+ rx.el.div(
+ _status_icon(STATUS_ICON_COLORS["Warning"]),
+ STATUS_VARIANT_TEXT["Warning"],
+ class_name=f"{DEFAULT_CLASS_NAME} {STATUS_TEXT_COLORS['Warning']}",
+ ),
+ ),
+ (
+ "Critical",
+ rx.el.div(
+ _status_icon(STATUS_ICON_COLORS["Critical"]),
+ STATUS_VARIANT_TEXT["Critical"],
+ class_name=f"{DEFAULT_CLASS_NAME} {STATUS_TEXT_COLORS['Critical']}",
+ ),
+ ),
+ rx.el.div(
+ _status_icon(STATUS_ICON_COLORS["Success"]),
+ STATUS_VARIANT_TEXT["Success"],
+ class_name=f"{DEFAULT_CLASS_NAME} {STATUS_TEXT_COLORS['Success']}",
+ ),
+ ),
+ href=STATUS_WEB_URL,
+ target="_blank",
+ rel="noopener noreferrer",
+ )
diff --git a/packages/reflex-site-shared/src/reflex_site_shared/constants.py b/packages/reflex-site-shared/src/reflex_site_shared/constants.py
index 9b57cb5db5e..66747418c10 100644
--- a/packages/reflex-site-shared/src/reflex_site_shared/constants.py
+++ b/packages/reflex-site-shared/src/reflex_site_shared/constants.py
@@ -21,6 +21,7 @@
TWITTER_URL = "https://twitter.com/getreflex"
DISCORD_URL = "https://discord.gg/T5WSbC2YtQ"
ROADMAP_URL = "https://github.com/reflex-dev/reflex/issues/2727"
+STATUS_WEB_URL = "https://status.reflex.dev"
REFLEX_URL = "https://reflex.dev/"
REFLEX_DOMAIN_URL = "https://reflex.dev/"
@@ -36,3 +37,9 @@
RECENT_BLOGS_API_URL: str = os.environ.get(
"RECENT_BLOGS_API_URL", "https://reflex.dev/api/v1/recent-blogs"
)
+
+
+CHECKLY_API_BASE_URL: str = "https://api.checklyhq.com/v1"
+CHECKLY_ACCOUNT_ID = os.environ.get("CHECKLY_ACCOUNT_ID", "")
+CHECKLY_API_KEY = os.environ.get("CHECKLY_API_KEY", "")
+CHECKLY_CHECK_GROUP_ID = os.environ.get("CHECKLY_CHECK_GROUP_ID", "")
diff --git a/packages/reflex-site-shared/src/reflex_site_shared/views/footer.py b/packages/reflex-site-shared/src/reflex_site_shared/views/footer.py
index c63bbb39cf8..834361dbf51 100644
--- a/packages/reflex-site-shared/src/reflex_site_shared/views/footer.py
+++ b/packages/reflex-site-shared/src/reflex_site_shared/views/footer.py
@@ -8,7 +8,9 @@
import reflex as rx
from reflex.style import color_mode, set_color_mode
from reflex_site_shared.backend.signup import IndexState
+from reflex_site_shared.backend.status import StatusState
from reflex_site_shared.components.icons import get_icon
+from reflex_site_shared.components.server_status import server_status
from reflex_site_shared.constants import (
CHANGELOG_URL,
DISCORD_URL,
@@ -62,7 +64,7 @@ def logo() -> rx.Component:
class_name="shrink-0 hidden dark:block",
),
href="/",
- class_name="block shrink-0 mr-[7rem] md:hidden xl:block",
+ class_name="block shrink-0 mr-[7rem] md:hidden xl:block h-fit",
)
@@ -297,11 +299,15 @@ def footer_index(class_name: str = "", grid_class_name: str = "") -> rx.Componen
class_name="flex flex-col max-lg:gap-6 lg:flex-row w-full",
),
rx.el.div(
- rx.el.span(
- f"Copyright © {datetime.now().year} Pynecone, Inc.",
- class_name="text-xs text-m-slate-7 dark:text-m-slate-6 font-medium",
+ server_status(StatusState.status),
+ rx.el.div(
+ rx.el.span(
+ f"Copyright © {datetime.now().year} Pynecone, Inc.",
+ class_name="text-xs text-m-slate-7 dark:text-m-slate-6 font-medium",
+ ),
+ menu_socials(),
+ class_name="flex flex-row items-center gap-6",
),
- menu_socials(),
rx.el.div(
class_name="absolute -top-px -right-24 w-24 h-px bg-gradient-to-l from-transparent to-current text-m-slate-4 dark:text-m-slate-10 max-lg:hidden"
),