From 19e23662dac0702adb225a3e70aa1fef5027d8b5 Mon Sep 17 00:00:00 2001 From: Abigail Emery Date: Tue, 7 Apr 2026 10:19:22 +0000 Subject: [PATCH 1/7] Add basic server landing page at root --- src/blueapi/service/main.py | 78 ++++++++++++++++++--- templates/index.html | 135 ++++++++++++++++++++++++++++++++++++ 2 files changed, 203 insertions(+), 10 deletions(-) create mode 100644 templates/index.html diff --git a/src/blueapi/service/main.py b/src/blueapi/service/main.py index 432cc5455..cf797532b 100644 --- a/src/blueapi/service/main.py +++ b/src/blueapi/service/main.py @@ -1,3 +1,4 @@ +import json import logging import urllib.parse from collections.abc import Awaitable, Callable @@ -11,6 +12,7 @@ Body, Depends, FastAPI, + Form, HTTPException, Request, Response, @@ -18,7 +20,8 @@ ) from fastapi.datastructures import Address from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import RedirectResponse, StreamingResponse +from fastapi.responses import HTMLResponse, RedirectResponse, StreamingResponse +from fastapi.templating import Jinja2Templates from observability_utils.tracing import ( add_span_attributes, get_tracer, @@ -163,15 +166,6 @@ async def on_token_error_401(_: Request, __: Exception): ) -@secure_router.get("/", include_in_schema=False) -def root_redirect() -> RedirectResponse: - """Redirect to docs url""" - return RedirectResponse( - status_code=status.HTTP_307_TEMPORARY_REDIRECT, - url=ApplicationConfig.DOCS_ENDPOINT, - ) - - @secure_router_v1.get("/environment", tags=[Tag.ENV]) @secure_router.get("/environment", tags=[Tag.ENV]) @start_as_current_span(TRACER, "runner") @@ -610,3 +604,67 @@ async def log_request_details( log(log_message, extra=extra) return response + + +templates = Jinja2Templates(directory="templates") + + +@secure_router.get("/", include_in_schema=False, response_class=HTMLResponse) +def root_landing( + request: Request, + runner: Annotated[WorkerDispatcher, Depends(_runner)], +) -> HTMLResponse: + + if runner._config.env.metadata: + instrument = runner._config.env.metadata.instrument + else: + instrument = "" + + devices = runner.run(interface.get_devices) + devices = [ + {"device": device.name, "protocols": [p.name for p in device.protocols]} + for device in devices + ] + + plans = runner.run(interface.get_plans) + task_list = get_tasks(runner) + + context = { + "instrument": instrument, + "devices": devices, + "plans": plans, + "tasks": task_list.tasks, + } + + return templates.TemplateResponse( + request=request, name="index.html", context=context + ) + + +@secure_router_v1.post("/run", include_in_schema=True, tags=[Tag.TASK]) +@start_as_current_span(TRACER) +def run( + name: Annotated[str, Form()], + params: Annotated[str, Form()], + instrument_session: Annotated[str, Form()], + request: Request, + response: Response, + runner: Annotated[WorkerDispatcher, Depends(_runner)], +) -> RedirectResponse: + + task_request = TaskRequest( + name=name, + params=json.loads(params), # do this validator in the model? + instrument_session=instrument_session, + ) + res = submit_task(request, response, task_request, runner) + + tid = res.task_id + req_task = WorkerTask(task_id=tid) + + try: + set_active_task(request, req_task, runner) + except HTTPException: + delete_submitted_task(tid, runner) + + return RedirectResponse(status_code=status.HTTP_204_NO_CONTENT, url="/") diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 000000000..cf67aad85 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,135 @@ + + + + + + {{instrument}}-blueapi + + + +

{{instrument}}-blueapi

+

+ api docs available at + /docs. +

+ +
+ +
+

Run Plan

+
+
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+ +

Tasks

+ +

Current task:

+ {% for task in tasks if not task.is_pending and not task.is_complete %} +
+
{{task.task_id}} +
    +
  • {{task.task.name}}
  • +
  • {{task.task.params}}
  • +
+
+
+ {% endfor %} + +

Pending tasks:

+ {% for task in tasks if task.is_pending %} +
+
{{task.task_id}} +
    +
  • {{task.task.name}}
  • +
  • {{task.task.params}}
  • +
+
+
+ {% endfor %} + +

Completed tasks:

+ {% for task in tasks if task.is_complete%} +
+
{{task.task_id}} +
    +
  • {{task.task.name}}
  • +
  • {{task.task.params}}
  • +
  • outcome: {{task.outcome.outcome}}
  • +
  • result: {{task.outcome.result}}
  • +
+
+
+ {% endfor %} + +
+ +
+

Plans

+ {% for plan in plans %} +

{{plan.name}}

+
+
Description:
+
{{plan.description}}
+
Plan Parameters:
+ {% for p in plan.parameter_schema.properties %} +
{{ p }}
+ {% endfor %} +
+ {% endfor %} +
+ +
+

Devices

+ {% for device in devices %} +

{{device.device}}

+
+
Protocols:
+
{{device.protocols}}
+
+ {% endfor %} +
+ +
+ + + From 68c3a732d2bbaf5877259325bd8b0b9dd0359498 Mon Sep 17 00:00:00 2001 From: Abigail Emery Date: Wed, 22 Apr 2026 16:45:11 +0000 Subject: [PATCH 2/7] Add favicon --- src/blueapi/service/main.py | 12 ++- templates/blueapi-logo.svg | 200 ++++++++++++++++++++++++++++++++++++ templates/index.html | 1 + 3 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 templates/blueapi-logo.svg diff --git a/src/blueapi/service/main.py b/src/blueapi/service/main.py index cf797532b..ef3f56ca7 100644 --- a/src/blueapi/service/main.py +++ b/src/blueapi/service/main.py @@ -20,7 +20,12 @@ ) from fastapi.datastructures import Address from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import HTMLResponse, RedirectResponse, StreamingResponse +from fastapi.responses import ( + FileResponse, + HTMLResponse, + RedirectResponse, + StreamingResponse, +) from fastapi.templating import Jinja2Templates from observability_utils.tracing import ( add_span_attributes, @@ -641,6 +646,11 @@ def root_landing( ) +@open_router.get("/favicon", include_in_schema=False) +async def favicon(): + return FileResponse("templates/blueapi-logo.svg") + + @secure_router_v1.post("/run", include_in_schema=True, tags=[Tag.TASK]) @start_as_current_span(TRACER) def run( diff --git a/templates/blueapi-logo.svg b/templates/blueapi-logo.svg new file mode 100644 index 000000000..98fa498d0 --- /dev/null +++ b/templates/blueapi-logo.svg @@ -0,0 +1,200 @@ + + + + + + + + + + + + + + + + + + + + Bluesky_Logo_Final + + + + Bluesky_Logo_Final + + + + + + + + + + + + + + + + + + + + diff --git a/templates/index.html b/templates/index.html index cf67aad85..71517e98a 100644 --- a/templates/index.html +++ b/templates/index.html @@ -3,6 +3,7 @@ + {{instrument}}-blueapi - - - - - - - - - - - - - Bluesky_Logo_Final - - - - Bluesky_Logo_Final - - - - - - - - - - - - - - - - - - - - From da168505e471bb09ac2a9101f3e4553f3a1e0d79 Mon Sep 17 00:00:00 2001 From: Abigail Emery Date: Wed, 24 Jun 2026 14:25:36 +0000 Subject: [PATCH 6/7] Add method to retrive associated instrument from runner --- src/blueapi/service/runner.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/blueapi/service/runner.py b/src/blueapi/service/runner.py index 2b5a5f37f..19f747f1c 100644 --- a/src/blueapi/service/runner.py +++ b/src/blueapi/service/runner.py @@ -149,6 +149,11 @@ def run( def state(self) -> EnvironmentResponse: return self._state + def instrument(self) -> str: + return ( + md.instrument if (md := self._config.env.metadata) is not None else "" + ) + class InvalidRunnerStateError(Exception): def __init__(self, message): From 5357ee3b90e1e8e52defc7faf1d418a36160dda0 Mon Sep 17 00:00:00 2001 From: Abigail Emery Date: Wed, 24 Jun 2026 15:15:52 +0000 Subject: [PATCH 7/7] Reduce plan params to just function args --- src/blueapi/service/main.py | 53 ++++++++++++++++++++++++++------ templates/index.html | 61 +++++++++++++++++++++++++++---------- 2 files changed, 89 insertions(+), 25 deletions(-) diff --git a/src/blueapi/service/main.py b/src/blueapi/service/main.py index 91c3f2ea8..7c9b77afd 100644 --- a/src/blueapi/service/main.py +++ b/src/blueapi/service/main.py @@ -3,7 +3,8 @@ import urllib.parse from collections.abc import Awaitable, Callable from contextlib import asynccontextmanager -from typing import Annotated +from dataclasses import dataclass +from typing import Annotated, Any import jwt from fastapi import ( @@ -34,7 +35,7 @@ ) from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor from opentelemetry.trace import get_tracer_provider -from pydantic import ValidationError +from pydantic import Field, ValidationError from pydantic.json_schema import SkipJsonSchema from starlette.responses import JSONResponse from super_state_machine.errors import TransitionError @@ -46,6 +47,7 @@ ObservabilityContextPropagator, VersionHeaders, ) +from blueapi.utils.base_model import BlueapiBaseModel from blueapi.worker import TrackableTask, WorkerState from blueapi.worker.event import TaskStatusEnum @@ -620,11 +622,6 @@ def root_landing( runner: Annotated[WorkerDispatcher, Depends(_runner)], ) -> HTMLResponse: - if runner._config.env.metadata: - instrument = runner._config.env.metadata.instrument - else: - instrument = "" - devices = runner.run(interface.get_devices) devices = [ {"device": device.name, "protocols": [p.name for p in device.protocols]} @@ -632,12 +629,50 @@ def root_landing( ] plans = runner.run(interface.get_plans) + + @dataclass() + class TmpModel: + name: str + description: str | None + parameter_schema: dict[str, Any] + + format_plans: list[TmpModel] = [] + for plan in plans: + sch: dict[str, Any] = plan.parameter_schema + plan_args: dict[str, Any] | None = sch.get("properties") + + args = {} + if plan_args: + for k, v in plan_args.items(): + if any_of_type := v.get("anyOf"): + tp_list = [] + for typ in any_of_type: + if list_type := typ.get("items"): + tp_list.append(f"list[{list_type.get('type')}]") + elif simple_type := typ.get("type"): + tp_list.append(simple_type) + tp = " | ".join(tp_list) + + elif list_type := v.get("items"): + tp = f"list[{list_type.get('type')}]" + else: + tp = v.get("type") + + args[f"{k}"] = tp + + p = TmpModel( + name=plan.name, + description=plan.description, + parameter_schema=args, + ) + format_plans.append(p) + task_list = get_tasks(runner) context = { - "instrument": instrument, + "instrument": runner.instrument(), "devices": devices, - "plans": plans, + "plans": format_plans, "tasks": task_list.tasks, } diff --git a/templates/index.html b/templates/index.html index ecd366630..7e67e4aa2 100644 --- a/templates/index.html +++ b/templates/index.html @@ -6,22 +6,50 @@ {{instrument}}-blueapi