diff --git a/requirements/SWR_SYSTEM_GUI_STATUS_PAGE_1.md b/requirements/SWR_SYSTEM_GUI_STATUS_PAGE_1.md deleted file mode 100644 index 8da62c7d..00000000 --- a/requirements/SWR_SYSTEM_GUI_STATUS_PAGE_1.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -itemId: SWR-SYSTEM-GUI-STATUS-PAGE-1 -itemTitle: Per-Environment Betterstack Status Page in Launchpad -itemHasParent: SHR-SYSTEM-1 -itemType: Requirement -Requirement type: FUNCTIONAL -Module: System -Layer: GUI ---- - -As a Launchpad user, I expect the embedded Betterstack status badge and the "Check Platform Status" link to reflect only the Aignostics Platform environment my Launchpad is connected to (as configured by `AIGNOSTICS_API_ROOT`), so that I can assess the operational health of the services I actually depend on without being distracted by, or misled by, the health of unrelated environments. - -The Launchpad shall resolve the public Betterstack status page URL from the configured platform environment as follows: - -- when connected to the production environment (`AIGNOSTICS_API_ROOT` = `https://platform.aignostics.com`), the Launchpad shall embed the badge of, and link to, `https://status.platform.aignostics.com`; -- when connected to the staging environment (`AIGNOSTICS_API_ROOT` = `https://platform-staging.aignostics.com`), the Launchpad shall embed the badge of, and link to, `https://status.platform-staging.aignostics.com`; -- when connected to a dev or test environment, or to any other environment for which no per-environment public Betterstack status page is configured, the Launchpad shall not render the Betterstack badge in its footer and shall not render the "Check Platform Status" item in its right-side menu. - -The user shall be able to override the resolved status page URL through the `AIGNOSTICS_STATUS_PAGE_URL` environment variable or the equivalent constructor argument, including overriding it to an empty value to suppress the badge and link. - -The Launchpad shall validate any user-supplied status page URL at configuration time and reject values that are not well-formed http(s) URLs or that contain characters that could break out of an HTML attribute when rendered (`"`, `'`, `<`, `>`, backtick, backslash, whitespace), so that the Launchpad cannot be tricked into rendering attacker-controlled markup through this configuration. - -When the Launchpad does not render the badge or the menu link, no degraded-state placeholder is shown — both surfaces are simply omitted from the layout. diff --git a/specifications/SPEC-LAUNCHPAD-STATUS-PAGE.md b/specifications/SPEC-LAUNCHPAD-STATUS-PAGE.md deleted file mode 100644 index 49d9b446..00000000 --- a/specifications/SPEC-LAUNCHPAD-STATUS-PAGE.md +++ /dev/null @@ -1,194 +0,0 @@ ---- -itemId: SPEC-LAUNCHPAD-STATUS-PAGE -itemTitle: Per-Environment Betterstack Status Page in Launchpad -itemType: Software Item Spec -itemFulfills: SWR-SYSTEM-GUI-STATUS-PAGE-1 -itemIsRelatedTo: SPEC-GUI-SERVICE, SPEC-PLATFORM-SERVICE, SPEC-SYSTEM-SERVICE -Module: System -Layer: GUI / Platform Service -Version: 1.0.0 -Date: 2026-04-26 ---- - -## 1. Description - -### 1.1 Purpose - -This specification describes how the Aignostics Launchpad (Desktop Application, NiceGUI-based) renders the embedded Betterstack status badge in its footer and the "Check Platform Status" link in its right-side menu so that both reflect only the Aignostics Platform environment the Launchpad is currently connected to (i.e., the environment selected by `AIGNOSTICS_API_ROOT`). - -The motivation is that the legacy aggregate page at `https://status.aignostics.com` covers production *and* staging *and* unrelated services (Console, Portal, Career Site, Website). A user running the Launchpad against a single environment is best served by the corresponding **narrower** Betterstack property of that same environment, with no badge or link rendered when no per-environment Betterstack property exists (dev, test, or unknown environments). - -### 1.2 Functional Requirements - -The Launchpad shall: - -- **[FR-01]** Resolve the public Betterstack status page URL from the configured `api_root` of the platform `Settings` model. -- **[FR-02]** Use `https://status.platform.aignostics.com` for production (`https://platform.aignostics.com`) and `https://status.platform-staging.aignostics.com` for staging (`https://platform-staging.aignostics.com`). -- **[FR-03]** Use `None` (i.e., no public per-environment status page) for the dev environment (`https://platform-dev.aignostics.ai`) and the test environment (`https://platform-test.aignostics.ai`), and for any unknown `api_root` whose auth fields are otherwise fully provided. -- **[FR-04]** Allow the user to override the resolved value through the `AIGNOSTICS_STATUS_PAGE_URL` environment variable or the `status_page_url` constructor argument of `Settings`. An empty string is treated as `None`. -- **[FR-05]** Validate the resolved value at `Settings` construction time, rejecting values that are not well-formed http(s) URLs and values that contain `"`, `'`, `<`, `>`, backtick, backslash, or whitespace characters. -- **[FR-06]** When the resolved value is non-`None`, render the Betterstack badge in the footer (as a 250×30 iframe pointing at `/badge?theme=dark`) and a "Check Platform Status" link in the right-side menu pointing at ``. -- **[FR-07]** When the resolved value is `None`, omit the Betterstack badge from the footer and omit the "Check Platform Status" item from the right-side menu — no degraded-state placeholder is rendered. -- **[FR-08]** Refresh the Betterstack iframe every 30 seconds (in alignment with the existing health-update interval), guarded so the refresh is a safe no-op when the iframe is absent from the DOM. - -### 1.3 Non-Functional Requirements - -- **Security**: User-controlled values must not be able to inject markup into the Launchpad webview. Defence-in-depth: (1) `Settings.status_page_url` is validated by `_validate_optional_url` before reaching the GUI layer; (2) the iframe is rendered via NiceGUI's `ui.element('iframe')` with attributes assigned through the props dict, so attribute values flow through Vue data binding rather than raw HTML construction. -- **Backwards compatibility**: An unknown `api_root` (with all auth fields provided) must produce a safe default (`None`, no badge, no link) rather than raising an error. The aggregate `https://status.aignostics.com` page must remain unchanged and reachable for users who navigate to it directly. -- **Resilience**: The 30-second iframe-refresh JS must remain safe when the iframe is absent from the DOM (dev/test or override-to-`None` cases). The behaviour shall not depend on the order in which the timer first fires relative to first DOM mount. - -### 1.4 Constraints and Limitations - -- The dev and test environments do not currently have a dedicated public Betterstack property; this specification deliberately treats that as a "no badge, no link" state, not an error. -- The `Settings` `pre_init` model validator returns early when all auth fields are explicitly provided. In that path, the per-environment match block is skipped, and `status_page_url` retains its declared default (`None`) unless the caller supplied it explicitly. - ---- - -## 2. Architecture and Design - -### 2.1 Files Touched - -| File | Role | -| --- | --- | -| `src/aignostics/platform/_constants.py` | Per-environment URL constants `STATUS_PAGE_URL_DEV`, `STATUS_PAGE_URL_TEST`, `STATUS_PAGE_URL_STAGING`, `STATUS_PAGE_URL_PRODUCTION`. | -| `src/aignostics/platform/_settings.py` | `Settings.status_page_url: str \| None` field with `BeforeValidator(_validate_optional_url)`; resolution inside the existing `pre_init` `match...case` block alongside the auth endpoints; helper `_validate_optional_url(value: str \| None) -> str \| None`. | -| `src/aignostics/platform/__init__.py` | Re-exports the four `STATUS_PAGE_URL_*` constants for downstream consumers. | -| `src/aignostics/gui/_frame.py` | Reads `settings().status_page_url` once after the context manager `yield`. Conditionally renders the right-menu "Check Platform Status" item, the footer iframe, and the 30-s refresh JS based on this value. Defensive JS element guard `if (iframe) { iframe.src = iframe.src; }` so the refresh never throws when the iframe is absent. | -| `tests/aignostics/platform/settings_test.py` | Per-environment assertions on `status_page_url` and parametrised rejection of invalid/unsafe URLs. | - -### 2.2 Resolution Algorithm - -```text -input: api_root (string), explicit overrides (env var, constructor argument) -output: status_page_url: str | None - -1. If the user provided `status_page_url` explicitly (constructor arg or - `AIGNOSTICS_STATUS_PAGE_URL` env var): - → run `_validate_optional_url`; on success use that value. -2. Else, in the existing `pre_init` `match...case`: - - api_root == API_ROOT_DEV → setdefault to STATUS_PAGE_URL_DEV (None) - - api_root == API_ROOT_TEST → setdefault to STATUS_PAGE_URL_TEST (None) - - api_root == API_ROOT_STAGING → setdefault to STATUS_PAGE_URL_STAGING - - api_root == API_ROOT_PRODUCTION → setdefault to STATUS_PAGE_URL_PRODUCTION - - any other api_root with all auth fields supplied: - → field default applies (None) - - any other api_root without auth fields: - → ValueError UNKNOWN_ENDPOINT_URL -``` - -### 2.3 Validation - -`_validate_optional_url(value: str | None) -> str | None` is registered as a Pydantic `BeforeValidator` on the field: - -1. `None` → `None`. -2. `""` → `None` (env-var loaders may produce an empty string when the variable is set but empty; treating it as `None` matches the dev/test default). -3. Non-empty string: - 1. Reject if it contains any of `"`, `'`, `<`, `>`, backtick, backslash, or whitespace (RFC 3986 requires those to be percent-encoded; raw forms are either malformed or an injection attempt). - 2. Otherwise, delegate to the existing `_validate_url` (scheme must be `http` or `https`; netloc must be non-empty). - -### 2.4 Rendering - -In `gui/_frame.py`: - -```python -status_page_url = settings().status_page_url # resolved once, reused - -if status_page_url: - # right-menu: "Check Platform Status" item with ui.link(...) - -if status_page_url: - # footer: NiceGUI iframe element, attributes via props dict (no raw HTML) - iframe = ui.element("iframe") - iframe.props["id"] = "betterstack" - iframe.props["src"] = urljoin(status_page_url + "/", "badge?theme=dark") - iframe.props["width"] = "250" - iframe.props["height"] = "30" - iframe.props["frameborder"] = "0" - iframe.props["scrolling"] = "no" - iframe.style("color-scheme: dark; margin-left: 0px;") - -# 30-s refresh, runs unconditionally; element existence is guarded in JS. -ui.run_javascript( - "var iframe = document.getElementById('betterstack');" - "if (iframe) { iframe.src = iframe.src; }" -) -``` - -The iframe is rendered as a NiceGUI `ui.element('iframe')` rather than `ui.html('