#!/usr/bin/env python3
from __future__ import annotations
from pathlib import Path
import sys
from fastapi import FastAPI, HTTPException, Form
from fastapi.responses import HTMLResponse, RedirectResponse
from pydantic import BaseModel
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from lib.life_noc_inputs import ( # noqa: E402
list_items,
get_item,
set_item,
complete_item,
get_defined_service,
compute_state_for_item,
)
class SetInputRequest(BaseModel):
value: str
origin: str = "manual"
class CompleteInputRequest(BaseModel):
origin: str = "manual"
app = FastAPI(title="Life-NOC Input API", version="1.0.0")
def html_escape(value: str) -> str:
return (
str(value)
.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace('"', """)
)
def link_row(label: str, url: str) -> str:
if not url:
return ""
u = html_escape(url)
l = html_escape(label)
return f'
{l}'
def resolve_instructions_url(domain: str, item_key: str, service: dict) -> str:
explicit = str(service.get("instructions_url", "")).strip()
if explicit:
return explicit
return f"/life-noc/item/{domain}/{item_key}"
@app.get("/health")
def health() -> dict:
return {"status": "ok"}
@app.get("/inputs/{domain}")
def api_list_inputs(domain: str) -> dict:
return {"domain": domain, "items": list_items(domain)}
@app.get("/inputs/{domain}/{item_key}")
def api_get_input(domain: str, item_key: str) -> dict:
try:
item = get_item(domain, item_key)
except KeyError:
raise HTTPException(status_code=404, detail="item introuvable")
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc))
return {"domain": domain, "item_key": item_key, **item}
@app.post("/inputs/{domain}/{item_key}")
def api_set_input(domain: str, item_key: str, req: SetInputRequest) -> dict:
item = set_item(domain, item_key, req.value, origin=req.origin)
return {"domain": domain, "item_key": item_key, **item}
@app.post("/inputs/{domain}/{item_key}/complete")
def api_complete_input(domain: str, item_key: str, req: CompleteInputRequest | None = None) -> dict:
origin = "manual" if req is None else req.origin
item = complete_item(domain, item_key, origin=origin)
return {"domain": domain, "item_key": item_key, **item}
@app.get("/life-noc/item/{domain}/{item_key}", response_class=HTMLResponse)
def page_item(domain: str, item_key: str) -> HTMLResponse:
try:
service = get_defined_service(domain, item_key)
current_input = get_item(domain, item_key)
state = compute_state_for_item(domain, item_key)
except KeyError:
raise HTTPException(status_code=404, detail="item introuvable")
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc))
title = service.get("title") or service.get("display_name") or item_key.replace("-", " ").capitalize()
summary = service.get("summary", "")
notes = service.get("notes", "")
instructions = service.get("instructions", "")
notes_url = service.get("notes_url", "")
instructions_url = resolve_instructions_url(domain, item_key, service)
action_url = service.get("action_url", "")
probe_type = service.get("probe", {}).get("type", "unknown")
ui = service.get("ui", {}) if isinstance(service.get("ui", {}), dict) else {}
form_mode = ui.get("form_mode", "")
allow_complete = bool(ui.get("allow_complete", probe_type == "elapsed_time"))
allow_manual_edit = bool(ui.get("allow_manual_edit", True))
links_html = ""
for fragment in [
link_row("Notes", notes_url),
link_row("Instructions", instructions_url),
link_row("Action", action_url),
]:
if fragment:
links_html += fragment
summary_html = ""
if summary:
summary_html = f'Résumé : {html_escape(summary)}
'
instructions_html = ""
if instructions:
instructions_html = f"""
Instructions
{html_escape(instructions)}
"""
manual_form_html = ""
if allow_manual_edit and form_mode == "complete_date":
manual_form_html = f"""
"""
complete_form_html = ""
if allow_complete:
complete_form_html = f"""
"""
html = f"""
Life-NOC — {html_escape(title)}
{html_escape(title)}
État actuel : {html_escape(state)}
Type de sonde : {html_escape(probe_type)}
{summary_html}
Dernier intrant
Valeur : {html_escape(current_input.get("value", ""))}
Capturé le : {html_escape(current_input.get("captured_at", ""))}
Origine : {html_escape(current_input.get("origin", ""))}
Contexte
Domaine : {html_escape(domain)}
Item key : {html_escape(item_key)}
Notes : {html_escape(notes)}
{instructions_html}
{manual_form_html}
{complete_form_html}
"""
return HTMLResponse(content=html)
@app.post("/life-noc/item/{domain}/{item_key}/set")
def page_set_item(domain: str, item_key: str, value: str = Form(...), origin: str = Form(default="manual")) -> RedirectResponse:
try:
set_item(domain, item_key, value=value, origin=origin)
except KeyError:
raise HTTPException(status_code=404, detail="item introuvable")
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc))
return RedirectResponse(
url=f"/life-noc/item/{domain}/{item_key}",
status_code=303,
)
@app.post("/life-noc/item/{domain}/{item_key}/complete")
def page_complete_item(domain: str, item_key: str, origin: str = Form(default="manual")) -> RedirectResponse:
try:
complete_item(domain, item_key, origin=origin)
except KeyError:
raise HTTPException(status_code=404, detail="item introuvable")
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc))
return RedirectResponse(
url=f"/life-noc/item/{domain}/{item_key}",
status_code=303,
)