Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/engineering_platform/assets/dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -3598,6 +3598,13 @@ function showComponentModal(payload) {
componentDetailField(fields, t("component.current_memory"), componentMemory(payload.processes));
} else {
componentDetailField(fields, t("component.process_status"), componentProcessStatus(payload));
if (payload.process_state === "IN_PROCESS") {
componentDetailField(
fields,
t("component.ep_server_uptime"),
formatComponentUptime(payload.process_host?.uptime_seconds),
);
}
}
componentDetailField(fields, t("component.runtime_path"), installation.runtime_path);
componentDetailField(fields, t("component.central_data_path"), installation.central_data_path);
Expand Down
10 changes: 5 additions & 5 deletions src/engineering_platform/assets/dashboard_locales.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3739,11 +3739,11 @@ Object.assign(DASHBOARD_MESSAGES.nl, {"component.ep_server":"EP-server","compone
Object.assign(DASHBOARD_MESSAGES.de, {"component.ep_server":"EP-Server","component.platform_database":"Plattformdatenbank","component.lifecycle_worker":"Lebenszyklus-Worker","component.operations_console":"Operations-Konsole","component.dashboard_relay":"Dashboard-Relay"});
Object.assign(DASHBOARD_MESSAGES.fr, {"component.ep_server":"Serveur EP","component.platform_database":"Base de données de plateforme","component.lifecycle_worker":"Worker de cycle de vie","component.operations_console":"Console des opérations","component.dashboard_relay":"Relais du tableau de bord"});
Object.assign(DASHBOARD_MESSAGES.es, {"component.ep_server":"Servidor EP","component.platform_database":"Base de datos de plataforma","component.lifecycle_worker":"Trabajador de ciclo de vida","component.operations_console":"Consola de operaciones","component.dashboard_relay":"Relé del panel"});
Object.assign(DASHBOARD_MESSAGES.en, {"component.memory_unavailable":"Memory unavailable","component.owned_process_unavailable":"No active LaunchAgent process","component.process_status":"Process status","component.runs_in_ep_server":"Runs in EP Server (PID {pid})","component.runs_in_ep_server_unavailable":"Runs in EP Server; process status unavailable","component.storage_no_process":"Storage component; no process"});
Object.assign(DASHBOARD_MESSAGES.nl, {"component.memory_unavailable":"Geheugen niet beschikbaar","component.owned_process_unavailable":"Geen actief LaunchAgent-proces","component.process_status":"Processtatus","component.runs_in_ep_server":"Draait in EP-server (PID {pid})","component.runs_in_ep_server_unavailable":"Draait in EP-server; processtatus niet beschikbaar","component.storage_no_process":"Opslagcomponent; geen proces"});
Object.assign(DASHBOARD_MESSAGES.de, {"component.memory_unavailable":"Speicher nicht verfügbar","component.owned_process_unavailable":"Kein aktiver LaunchAgent-Prozess","component.process_status":"Prozessstatus","component.runs_in_ep_server":"Läuft im EP-Server (PID {pid})","component.runs_in_ep_server_unavailable":"Läuft im EP-Server; Prozessstatus nicht verfügbar","component.storage_no_process":"Speicherkomponente; kein Prozess"});
Object.assign(DASHBOARD_MESSAGES.fr, {"component.memory_unavailable":"Mémoire indisponible","component.owned_process_unavailable":"Aucun processus LaunchAgent actif","component.process_status":"État du processus","component.runs_in_ep_server":"S’exécute dans le serveur EP (PID {pid})","component.runs_in_ep_server_unavailable":"S’exécute dans le serveur EP ; état indisponible","component.storage_no_process":"Composant de stockage ; aucun processus"});
Object.assign(DASHBOARD_MESSAGES.es, {"component.memory_unavailable":"Memoria no disponible","component.owned_process_unavailable":"No hay proceso LaunchAgent activo","component.process_status":"Estado del proceso","component.runs_in_ep_server":"Se ejecuta en el servidor EP (PID {pid})","component.runs_in_ep_server_unavailable":"Se ejecuta en el servidor EP; estado no disponible","component.storage_no_process":"Componente de almacenamiento; sin proceso"});
Object.assign(DASHBOARD_MESSAGES.en, {"component.ep_server_uptime":"Shared EP Server uptime","component.memory_unavailable":"Memory unavailable","component.owned_process_unavailable":"No active LaunchAgent process","component.process_status":"Process status","component.runs_in_ep_server":"Runs in EP Server (PID {pid})","component.runs_in_ep_server_unavailable":"Runs in EP Server; process status unavailable","component.storage_no_process":"Storage component; no process"});
Object.assign(DASHBOARD_MESSAGES.nl, {"component.ep_server_uptime":"Gedeelde EP-server-uptime","component.memory_unavailable":"Geheugen niet beschikbaar","component.owned_process_unavailable":"Geen actief LaunchAgent-proces","component.process_status":"Processtatus","component.runs_in_ep_server":"Draait in EP-server (PID {pid})","component.runs_in_ep_server_unavailable":"Draait in EP-server; processtatus niet beschikbaar","component.storage_no_process":"Opslagcomponent; geen proces"});
Object.assign(DASHBOARD_MESSAGES.de, {"component.ep_server_uptime":"Geteilte EP-Server-Laufzeit","component.memory_unavailable":"Speicher nicht verfügbar","component.owned_process_unavailable":"Kein aktiver LaunchAgent-Prozess","component.process_status":"Prozessstatus","component.runs_in_ep_server":"Läuft im EP-Server (PID {pid})","component.runs_in_ep_server_unavailable":"Läuft im EP-Server; Prozessstatus nicht verfügbar","component.storage_no_process":"Speicherkomponente; kein Prozess"});
Object.assign(DASHBOARD_MESSAGES.fr, {"component.ep_server_uptime":"Durée de fonctionnement partagée du serveur EP","component.memory_unavailable":"Mémoire indisponible","component.owned_process_unavailable":"Aucun processus LaunchAgent actif","component.process_status":"État du processus","component.runs_in_ep_server":"S’exécute dans le serveur EP (PID {pid})","component.runs_in_ep_server_unavailable":"S’exécute dans le serveur EP ; état indisponible","component.storage_no_process":"Composant de stockage ; aucun processus"});
Object.assign(DASHBOARD_MESSAGES.es, {"component.ep_server_uptime":"Tiempo de actividad compartido del servidor EP","component.memory_unavailable":"Memoria no disponible","component.owned_process_unavailable":"No hay proceso LaunchAgent activo","component.process_status":"Estado del proceso","component.runs_in_ep_server":"Se ejecuta en el servidor EP (PID {pid})","component.runs_in_ep_server_unavailable":"Se ejecuta en el servidor EP; estado no disponible","component.storage_no_process":"Componente de almacenamiento; sin proceso"});
Object.assign(DASHBOARD_MESSAGES.en, {"log_event.provider_action_requested":"Dashboard provider action requested","log_event.provider_action_started":"Dashboard provider login started","log_event.provider_action_completed":"Dashboard provider action completed","log_event.provider_action_failed":"Dashboard provider action failed"});
Object.assign(DASHBOARD_MESSAGES.nl, {"log_event.provider_action_requested":"Dashboard-provideractie aangevraagd","log_event.provider_action_started":"Dashboard-providerlogin gestart","log_event.provider_action_completed":"Dashboard-provideractie voltooid","log_event.provider_action_failed":"Dashboard-provideractie mislukt"});
Object.assign(DASHBOARD_MESSAGES.de, {"log_event.provider_action_requested":"Dashboard-Anbieteraktion angefordert","log_event.provider_action_started":"Dashboard-Anbieteranmeldung gestartet","log_event.provider_action_completed":"Dashboard-Anbieteraktion abgeschlossen","log_event.provider_action_failed":"Dashboard-Anbieteraktion fehlgeschlagen"});
Expand Down
12 changes: 9 additions & 3 deletions src/engineering_platform/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -1273,19 +1273,24 @@ def _dashboard_relay_component(*, server_running: bool) -> dict[str, object]:
definition = PLATFORM_COMPONENT_BY_ID["dashboard_relay"]
label = definition.lifecycle_label
try:
runtime = LaunchdProvider().runtime_status(label) if label else None
lifecycle = LaunchdProvider()
runtime = lifecycle.runtime_status(label) if label else None
observed = lifecycle.runtime_details(label) if label else None
observed = observed if isinstance(observed, LaunchdRuntimeDetails) else None
relay_running = bool(runtime and runtime.qualified)
detail = runtime.detail if runtime is not None else "lifecycle owner unavailable"
except OSError:
relay_running, detail = False, "lifecycle owner unavailable"
observed, relay_running, detail = None, False, "lifecycle owner unavailable"
healthy = server_running and relay_running
return {
"healthy": healthy,
"status_code": definition.active_status if healthy else definition.inactive_status,
"detail_code": definition.detail_code,
"lifecycle_label": label,
"lifecycle_state": "RUNNING" if relay_running else "STOPPED",
"uptime_seconds": 0,
# A Relay has its own LaunchAgent process. Never substitute a
# placeholder (notably zero) for the observed process lifetime.
"uptime_seconds": observed.uptime_seconds if observed is not None else None,
"recent_error": None if healthy else detail,
}

Expand Down Expand Up @@ -1381,6 +1386,7 @@ def _platform_component_detail(data_root: Path, component_id: str) -> dict[str,
detail["process_host"] = {
"component": "ep_server",
"pid": host.pid if host is not None and host.active else None,
"uptime_seconds": host.uptime_seconds if host is not None and host.active else None,
}
return detail

Expand Down
15 changes: 14 additions & 1 deletion tests/engineering/dashboard.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7801,6 +7801,7 @@ test.describe("Engineering Status browser smoke", () => {
detail: "HTTP-dashboard reageert",
version: "1.2.87",
uptime_seconds: detailsRequest === 1 ? 10 : 20,
process_state: "OWNED_PROCESS",
processes: [{ pid: 42, memory_kib: detailsRequest === 1 ? 1024 : 2048 }],
launchd: {},
restart_supported: true,
Expand Down Expand Up @@ -7851,9 +7852,10 @@ test.describe("Engineering Status browser smoke", () => {
component: "lifecycle_worker",
healthy: true,
process_state: "IN_PROCESS",
process_host: { component: "ep_server", pid: 321 },
process_host: { component: "ep_server", pid: 321, uptime_seconds: 3725 },
}));
await expect(page.locator("#componentModalContent")).toContainText("ProcesstatusDraait in EP-server (PID 321)");
await expect(page.locator("#componentModalContent")).toContainText("Gedeelde EP-server-uptime1u 2m");

await page.locator("#componentModalClose").click();
await page.evaluate(() => showComponentModal({
Expand All @@ -7864,6 +7866,17 @@ test.describe("Engineering Status browser smoke", () => {
}));
await expect(page.locator("#componentModalContent")).toContainText("ProcesstatusOpslagcomponent; geen proces");
await expect(page.locator("#componentModalContent")).toContainText("Databasegrootte12,5 MB");

await page.locator("#componentModalClose").click();
await page.evaluate(() => showComponentModal({
component: "dashboard_relay",
healthy: true,
uptime_seconds: 30,
process_state: "OWNED_PROCESS",
processes: [{ pid: 654, memory_kib: 1024 }],
}));
await expect(page.locator("#componentModalContent")).toContainText("Uptime30s");
await expect(page.locator("#componentModalContent")).toContainText("Huidig geheugenPID 654: 1.0 MiB");
});

test("keeps the component-modal header inside its panel while details scroll", async ({ page }) => {
Expand Down
23 changes: 17 additions & 6 deletions tests/engineering/test_server_foundation.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,16 @@ def test_component_details_expose_only_the_relevant_installed_locations(self) ->
@patch("engineering_platform.server.LaunchdProvider")
def test_service_components_expose_their_launchagent_as_host_detail(self, launchd: object) -> None:
server.initialize(self.root)
launchd.return_value.runtime_details.side_effect = (
LaunchdRuntimeDetails("com.engineeringplatform.server", True, True, 321, "(never exited)", 2048, 61),
LaunchdRuntimeDetails("com.engineeringplatform.dashboard-relay", True, True, 654, "1", 1024, 30),
LaunchdRuntimeDetails("com.engineeringplatform.server", True, True, 321, "(never exited)", 2048, 61),
)
def runtime_details(label: str) -> LaunchdRuntimeDetails:
return {
"com.engineeringplatform.server": LaunchdRuntimeDetails(
"com.engineeringplatform.server", True, True, 321, "(never exited)", 2048, 61,
),
"com.engineeringplatform.dashboard-relay": LaunchdRuntimeDetails(
"com.engineeringplatform.dashboard-relay", True, True, 654, "1", 1024, 30,
),
}[label]
launchd.return_value.runtime_details.side_effect = runtime_details

with patch("engineering_platform.server._launch_agent_configuration", return_value={
"run_at_load": True, "keep_alive": True,
Expand All @@ -102,7 +107,9 @@ def test_service_components_expose_their_launchagent_as_host_detail(self, launch
self.assertEqual(ep_server["uptime_seconds"], 61)
self.assertEqual(relay["processes"], [{"pid": 654, "memory_kib": 1024}])
self.assertEqual(worker["process_state"], "IN_PROCESS")
self.assertEqual(worker["process_host"], {"component": "ep_server", "pid": 321})
self.assertEqual(worker["process_host"], {
"component": "ep_server", "pid": 321, "uptime_seconds": 61,
})

def test_launchagent_configuration_projects_boolean_and_dictionary_policies(self) -> None:
plist_path = self.root / "agent.plist"
Expand Down Expand Up @@ -559,10 +566,14 @@ def test_dashboard_relay_requires_the_real_launch_agent_and_server(self) -> None
launchd.return_value.runtime_status.return_value = ProviderStatus(
"launchd", "configured", True, "LaunchAgent process is active"
)
launchd.return_value.runtime_details.return_value = LaunchdRuntimeDetails(
"com.engineeringplatform.dashboard-relay", True, True, 654, None, 1024, 30,
)
component = server.status(self.root)["components"]["dashboard_relay"]
self.assertTrue(component["healthy"])
self.assertEqual(component["status_code"], "DASHBOARD_RELAY_ACTIVE")
self.assertEqual(component["lifecycle_state"], "RUNNING")
self.assertEqual(component["uptime_seconds"], 30)

with patch("engineering_platform.server._runtime", return_value={"pid": 73}), patch(
"engineering_platform.server._alive", return_value=True
Expand Down
Loading