diff --git a/docs/server/web-configuration.md b/docs/server/web-configuration.md index b3c2e18..2b8894d 100644 --- a/docs/server/web-configuration.md +++ b/docs/server/web-configuration.md @@ -20,12 +20,13 @@ Every environment variable `Ignis.Web` (the BFF) reads. ## Feature flags -Both default to off. Set to `"true"` to enable. +All default to off. Set to `"true"` to enable. -| Variable | Notes | -| -------------------------- | ------------------------------------------------------------------------------------------------------------------ | -| `IGNIS_WEB_FEATURES_ADMIN` | Enables the admin UI at `/admin/*`. Requires `IGNIS_WEB_FEATURES_AUTH=true`. See [Admin UI](../admin/admin-ui.md). | -| `IGNIS_WEB_FEATURES_AUTH` | Master switch for the OAuth/BFF login flow. Most other features require this. | +| Variable | Notes | +| ------------------------------- | ------------------------------------------------------------------------------------------------------------------ | +| `IGNIS_WEB_FEATURES_ADMIN` | Enables the admin UI at `/admin/*`. Requires `IGNIS_WEB_FEATURES_AUTH=true`. See [Admin UI](../admin/admin-ui.md). | +| `IGNIS_WEB_FEATURES_AUTH` | Master switch for the OAuth/BFF login flow. Most other features require this. | +| `IGNIS_WEB_FEATURES_OPERATIONS` | Enables the operations log at `/admin/operations`. Requires `IGNIS_WEB_FEATURES_ADMIN=true`. | ## Cross-references with the API diff --git a/infra/helm/charts/app/values.yaml b/infra/helm/charts/app/values.yaml index 91f2108..72f69a8 100644 --- a/infra/helm/charts/app/values.yaml +++ b/infra/helm/charts/app/values.yaml @@ -67,7 +67,7 @@ web: # Secrets — e.g. one synced by External Secrets, one with the session key. # Typical keys: IGNIS_AUTH_ISSUER, IGNIS_WEB_APP_URL, IGNIS_WEB_CLIENT_ID, # IGNIS_WEB_CLIENT_SECRET, IGNIS_WEB_SESSION_SECRET, IGNIS_WEB_FHIR_BASE_URL, - # IGNIS_WEB_FEATURES_AUTH, IGNIS_WEB_FEATURES_ADMIN. + # IGNIS_WEB_FEATURES_AUTH, IGNIS_WEB_FEATURES_ADMIN, IGNIS_WEB_FEATURES_OPERATIONS. existingSecrets: [] # Extra env vars to set inline (e.g. non-secret feature flags). # Format: list of {name, value} entries. diff --git a/src/Ignis.Web/.env.example b/src/Ignis.Web/.env.example index 6ee368d..68a3eb2 100644 --- a/src/Ignis.Web/.env.example +++ b/src/Ignis.Web/.env.example @@ -24,3 +24,4 @@ IGNIS_WEB_FHIR_BASE_URL=https://localhost:5201/fhir/ # Feature flags. Set to "true" to expose a feature. IGNIS_WEB_FEATURES_ADMIN=true IGNIS_WEB_FEATURES_AUTH=true +IGNIS_WEB_FEATURES_OPERATIONS=false diff --git a/src/Ignis.Web/app/features/operations/config.server.ts b/src/Ignis.Web/app/features/operations/config.server.ts new file mode 100644 index 0000000..4893a0d --- /dev/null +++ b/src/Ignis.Web/app/features/operations/config.server.ts @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2026, Incendi + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +import { envBool } from "#app/env.server"; +import * as adminConfig from "#app/features/admin/config.server"; + +export function isEnabled(): boolean { + // Operations lives under the admin surface — requires admin (which in turn + // requires auth) plus its own IGNIS_WEB_FEATURES_OPERATIONS flag. + return adminConfig.isEnabled() && envBool("IGNIS_WEB_FEATURES_OPERATIONS", { default: false }); +} diff --git a/src/Ignis.Web/app/features/operations/routes/index.tsx b/src/Ignis.Web/app/features/operations/routes/index.tsx new file mode 100644 index 0000000..c8519a3 --- /dev/null +++ b/src/Ignis.Web/app/features/operations/routes/index.tsx @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2026, Incendi + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +import { Heading } from "@eventuras/ratio-ui/core/Heading"; +import { Text } from "@eventuras/ratio-ui/core/Text"; +import { Unauthorized } from "@eventuras/ratio-ui/blocks/Unauthorized"; +import { Console } from "@eventuras/ratio-ui/console"; +import { Container } from "@eventuras/ratio-ui/layout/Container"; +import { Stack } from "@eventuras/ratio-ui/layout/Stack"; +import { redirect } from "react-router"; + +import { getSessionFromRequest } from "#app/features/auth/session.server"; +import { scopes } from "#app/features/admin/scopes"; +import { m } from "#app/i18n/paraglide/messages"; + +import type { Route } from "./+types/index"; +import { isEnabled } from "../config.server"; + +export async function loader({ request }: Route.LoaderArgs) { + if (!isEnabled()) return redirect("/"); + const session = await getSessionFromRequest(request); + if (session === null) return redirect("/auth/login"); + const grantedScopes = session.scopes ?? []; + return { isAuthorized: grantedScopes.includes(scopes.operationsRead) }; +} + +// Dummy stream — replaced with real SSE-backed events in a follow-up PR. +const dummyEvents = [ + { + id: 1, + hhmmss: "12:00:00", + ms: "123", + level: "info" as const, + source: "Import", + message: "Found 21 entries in archive.", + }, + { + id: 2, + hhmmss: "12:00:00", + ms: "456", + level: "debug" as const, + source: "Import", + message: "Processed account-example.json", + }, + { + id: 3, + hhmmss: "12:00:00", + ms: "789", + level: "success" as const, + source: "Spark", + message: "Imported Patient/example", + }, + { + id: 4, + hhmmss: "12:00:01", + ms: "123", + level: "warning" as const, + source: "Import", + message: "Skipping oversize entry: huge-bundle.json", + }, + { + id: 5, + hhmmss: "12:00:01", + ms: "456", + level: "error" as const, + source: "Import", + message: "Failed to ingest entry: codesystem-snomedct.json", + }, + { + id: 6, + hhmmss: "12:00:02", + ms: "789", + level: "success" as const, + source: "Import", + message: "Enumerated 21 entries. Imported 18, skipped 2, failed 1.", + }, +]; + +export default function OperationsIndex({ loaderData }: Route.ComponentProps) { + if (!loaderData.isAuthorized) { + return ; + } + + return ( + + + + {m.operations_title()} + {m.operations_subtitle()} + + + + + {m.operations_console_title()} + + {dummyEvents.length} events + + + + {dummyEvents.map((ev) => ( + } + level={ev.level} + source={ev.source} + message={ev.message} + /> + ))} + + + + {m.operations_console_status_dummy()} + + + ); +} diff --git a/src/Ignis.Web/app/i18n/messages/en.json b/src/Ignis.Web/app/i18n/messages/en.json index 62bf92a..d0e5a7a 100644 --- a/src/Ignis.Web/app/i18n/messages/en.json +++ b/src/Ignis.Web/app/i18n/messages/en.json @@ -17,5 +17,9 @@ "home_about": "Ignis is a platform for experimenting with FHIR (Fast Healthcare Interoperability Resources). This project builds on Spark, an open-source FHIR server implementation.", "home_subtitle": "FHIR experiments and prototyping", "home_title": "Ignis FHIR Sandbox", - "nav_login": "Login" + "nav_login": "Login", + "operations_title": "Operations", + "operations_subtitle": "Live progress for long-running server operations.", + "operations_console_title": "Operations log", + "operations_console_status_dummy": "Showing placeholder data — live wiring lands in a follow-up." } diff --git a/src/Ignis.Web/app/i18n/messages/nb.json b/src/Ignis.Web/app/i18n/messages/nb.json index d42ae78..63d346a 100644 --- a/src/Ignis.Web/app/i18n/messages/nb.json +++ b/src/Ignis.Web/app/i18n/messages/nb.json @@ -17,5 +17,9 @@ "home_about": "Ignis er en plattform for eksperimentering med FHIR (Fast Healthcare Interoperability Resources). Prosjektet bygger på Spark, en åpen kildekode FHIR-server.", "home_subtitle": "FHIR-eksperimenter og prototyping", "home_title": "Ignis FHIR-sandkasse", - "nav_login": "Logg inn" + "nav_login": "Logg inn", + "operations_title": "Operasjoner", + "operations_subtitle": "Sanntidsfremdrift for langvarige operasjoner på serveren.", + "operations_console_title": "Operasjonslogg", + "operations_console_status_dummy": "Viser plassholder-data — ekte tilkobling kommer i en oppfølger." } diff --git a/src/Ignis.Web/app/routes.ts b/src/Ignis.Web/app/routes.ts index 827ae24..eb04afb 100644 --- a/src/Ignis.Web/app/routes.ts +++ b/src/Ignis.Web/app/routes.ts @@ -13,6 +13,7 @@ export default [ ...prefix(":locale?", [ index("routes/home.tsx"), route("admin", "features/admin/routes/index.tsx"), + route("admin/operations", "features/operations/routes/index.tsx"), ]), route("healthz", "routes/healthz.ts"), route("auth/login", "features/auth/routes/login.tsx"),