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
11 changes: 6 additions & 5 deletions docs/server/web-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion infra/helm/charts/app/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions src/Ignis.Web/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
14 changes: 14 additions & 0 deletions src/Ignis.Web/app/features/operations/config.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
* Copyright (c) 2026, Incendi <info@incendi.no>
*
* 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 });
}
119 changes: 119 additions & 0 deletions src/Ignis.Web/app/features/operations/routes/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
* Copyright (c) 2026, Incendi <info@incendi.no>
*
* 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 <Unauthorized />;
}

return (
<Container as="main">
<Stack direction="vertical" gap="lg">
<Stack direction="vertical" gap="sm">
<Heading as="h1">{m.operations_title()}</Heading>
<Text>{m.operations_subtitle()}</Text>
</Stack>

<Console theme="dark" aria-label={m.operations_console_title()}>
<Console.TitleBar>
<Console.Title>{m.operations_console_title()}</Console.Title>
<Console.Counter>
<b>{dummyEvents.length}</b> events
</Console.Counter>
</Console.TitleBar>
<Console.Body>
{dummyEvents.map((ev) => (
<Console.Entry
key={ev.id}
timestamp={<Console.Time hhmmss={ev.hhmmss} ms={ev.ms} />}
level={ev.level}
source={ev.source}
message={ev.message}
/>
))}
</Console.Body>
</Console>

<Text>{m.operations_console_status_dummy()}</Text>
</Stack>
</Container>
);
}
6 changes: 5 additions & 1 deletion src/Ignis.Web/app/i18n/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
6 changes: 5 additions & 1 deletion src/Ignis.Web/app/i18n/messages/nb.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
1 change: 1 addition & 0 deletions src/Ignis.Web/app/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down