Add Reports drawer, Dell catalog compliance, and Service Tag chat context.

Exposes OME warranties, baselines, report defs, and CSV/JSON exports for customer-ready fleet briefs.
This commit is contained in:
mo
2026-07-17 15:15:57 +02:00
parent c2bb0465d0
commit 115b6e39aa
11 changed files with 1393 additions and 36 deletions
+712 -9
View File
@@ -35,10 +35,11 @@ class Settings(BaseSettings):
vllm_model: str = "llama3-70b-gptq"
vllm_max_model_len: int = 4096
vllm_max_tokens: int = 700
chat_system_chars: int = 5500
chat_system_chars: int = 9000
openwebui_email: str = ""
openwebui_password: str = ""
cockpit_data: str = "/data"
reports_cache_ttl: float = 600.0
class Config:
env_file = ".env"
@@ -78,6 +79,77 @@ _lock = asyncio.Lock()
DETAIL_CACHE: dict[int, dict] = {}
PREV_DEVICES: dict[int, dict] = {}
EVENT_FEED: list[dict] = []
REPORT_CACHE: dict[str, Any] = {
"warranties": None,
"warranties_ts": 0.0,
"compliance": None,
"compliance_ts": 0.0,
"baselines": None,
"baselines_ts": 0.0,
"catalogs": None,
"catalogs_ts": 0.0,
"report_defs": None,
"report_defs_ts": 0.0,
"jobs": None,
"jobs_ts": 0.0,
}
async def ome_session(client: httpx.AsyncClient) -> tuple[str, dict, str]:
"""Create an OME API session; returns (base, headers, session_id)."""
base = settings.ome_url.rstrip("/")
r = await client.post(
f"{base}/api/SessionService/Sessions",
json={
"UserName": settings.ome_user,
"Password": settings.ome_password,
"SessionType": "API",
},
)
r.raise_for_status()
token = r.headers.get("X-Auth-Token")
sid = str((r.json() or {}).get("Id") or "")
headers = {"X-Auth-Token": token, "Accept": "application/json"}
return base, headers, sid
async def ome_session_delete(client: httpx.AsyncClient, base: str, headers: dict, sid: str) -> None:
if not sid:
return
try:
await client.delete(f"{base}/api/SessionService/Sessions('{sid}')", headers=headers)
except Exception:
pass
def _cache_get(key: str) -> Any | None:
ts = float(REPORT_CACHE.get(f"{key}_ts") or 0)
if REPORT_CACHE.get(key) is not None and time.time() - ts < settings.reports_cache_ttl:
return REPORT_CACHE.get(key)
return None
def _cache_set(key: str, value: Any) -> Any:
REPORT_CACHE[key] = value
REPORT_CACHE[f"{key}_ts"] = time.time()
return value
def csv_escape(val: Any) -> str:
s = "" if val is None else str(val)
if any(c in s for c in (",", '"', "\n", "\r")):
return '"' + s.replace('"', '""') + '"'
return s
def rows_to_csv(rows: list[dict], columns: list[str] | None = None) -> str:
if not rows and not columns:
return ""
cols = columns or list(rows[0].keys())
lines = [",".join(cols)]
for row in rows:
lines.append(",".join(csv_escape(row.get(c)) for c in cols))
return "\n".join(lines) + "\n"
def subnet_of(ip: str | None) -> str:
@@ -235,7 +307,9 @@ async def ome_fetch() -> dict:
"id": d.get("Id"),
"name": d.get("DeviceName") or f"device-{d.get('Id')}",
"model": model,
"service_tag": d.get("DeviceServiceTag"),
"service_tag": d.get("DeviceServiceTag")
or d.get("Identifier")
or d.get("ChassisServiceTag"),
"type": dtype,
"sub_type": sub,
"connected": conn,
@@ -601,6 +675,22 @@ async def startup():
init_db()
asyncio.create_task(poll_loop())
asyncio.create_task(gpu_loop())
asyncio.create_task(warm_reports_cache())
async def warm_reports_cache():
"""Background warm of warranty/compliance for chat + inspector."""
await asyncio.sleep(8)
try:
await fetch_warranties()
await fetch_compliance()
log.info(
"Reports cache warmed: warranties=%s compliance_outdated=%s",
(REPORT_CACHE.get("warranties") or {}).get("count"),
((REPORT_CACHE.get("compliance") or {}).get("summary") or {}).get("outdated_devices"),
)
except Exception as e:
log.warning("Reports cache warm failed: %s", e)
@app.get("/api/health")
@@ -793,11 +883,32 @@ def build_fleet_context(focus_device_id: int | None = None, max_chars: int | Non
critical = [a for a in alerts if a.get("severity") == "Critical"][:8]
warnings = [a for a in alerts if a.get("severity") == "Warning"][:5]
hottest = (ctx.get("hottest") or [])[:5]
compliance = _cache_get("compliance") or REPORT_CACHE.get("compliance") or {}
comp_sum = (compliance or {}).get("summary") or {}
# SERVICE TAG INDEX first — must survive truncation
st_lines = [
"SERVICE TAG INDEX (use these Service Tags in every node answer):",
]
for d in sorted(devices, key=lambda x: (x.get("name") or "").lower()):
st = (d.get("service_tag") or "").strip() or "NONE"
st_lines.append(
"- ST={st} | {name} | ip={ip} | model={model} | connected={conn}".format(
st=st,
name=(d.get("name") or "")[:40],
ip=d.get("ip") or "",
model=(d.get("model") or "")[:28],
conn="yes" if d.get("connected") else "no",
)
)
st_block = "\n".join(st_lines)
lines = [
"You are OpenManage Cockpit Copilot for Dell ATC. Be concise and operational.",
"Admins: Jody van Dongen, Laurens Rammers, Mohamed El Kadi.",
"Use ONLY this live snapshot. Unknown => say unknown.",
"ALWAYS include Service Tag (ST=...), model, and IP when discussing any system.",
"If a Service Tag is missing in the index, say so explicitly.",
"",
"FLEET: total={t} connected={c} offline={o} watts={w} alerts_total={a}".format(
t=summary.get("total"),
@@ -806,11 +917,21 @@ def build_fleet_context(focus_device_id: int | None = None, max_chars: int | Non
w=summary.get("total_watts"),
a=summary.get("alerts_total"),
),
"CONNECTED:",
]
if comp_sum:
lines.append(
"FIRMWARE COMPLIANCE (Dell catalog baseline): outdated_devices={od} critical_components={cc} baseline={bn}".format(
od=comp_sum.get("outdated_devices"),
cc=comp_sum.get("critical_components"),
bn=(comp_sum.get("baseline_name") or "")[:40],
)
)
lines.append("CONNECTED:")
for d in connected[:18]:
lines.append(
"- {name} ip={ip} model={model} W={watts} st={status}".format(
"- ST={st} {name} ip={ip} model={model} W={watts} status={status}".format(
st=(d.get("service_tag") or "NONE"),
name=(d.get("name") or "")[:36],
ip=d.get("ip"),
model=(d.get("model") or "")[:28],
@@ -885,24 +1006,33 @@ def build_fleet_context(focus_device_id: int | None = None, max_chars: int | Non
lines.append("FOCUS:")
if node:
lines.append(
"{name} id={id} ip={ip} model={model} W={watts} connected={conn} status={st}".format(
"ST={st} {name} id={id} ip={ip} model={model} W={watts} connected={conn} status={stt}".format(
st=(node.get("service_tag") or "NONE"),
name=node.get("name"),
id=node.get("id"),
ip=node.get("ip"),
model=node.get("model"),
watts=node.get("watts"),
conn=node.get("connected"),
st=node.get("status"),
stt=node.get("status"),
)
)
related = [a for a in alerts if a.get("device_id") == focus_device_id][:5]
for a in related:
lines.append("alert {sev}: {msg}".format(sev=a.get("severity"), msg=(a.get("message") or "")[:100]))
lines.append(
"alert {sev}: {msg}".format(sev=a.get("severity"), msg=(a.get("message") or "")[:100])
)
else:
lines.append("device_id=%s missing" % focus_device_id)
lines.append("Reply with concrete names, IPs, and next actions.")
out = "\n".join(lines)
lines.append(
"Reply with concrete Service Tags, names, IPs, models, and next actions."
)
body = "\n".join(lines)
budget = max(800, limit - len(st_block) - 40)
if len(body) > budget:
body = body[: budget - 20] + "\n…[truncated]"
out = st_block + "\n\n" + body
if len(out) > limit:
out = out[: limit - 20] + "\n…[truncated]"
return out
@@ -1489,6 +1619,574 @@ async def ws_ssh(ws: WebSocket):
log.info("SSH session end id=%s target=%s active=%s", session_id, client_host, len(SSH_SESSIONS))
# ---------------------------------------------------------------------------
# Reports / compliance / warranty / export (read-only OME surfaces)
# ---------------------------------------------------------------------------
async def fetch_warranties(force: bool = False) -> dict:
if not force:
cached = _cache_get("warranties")
if cached is not None:
return cached
rows: list[dict] = []
async with httpx.AsyncClient(verify=False, timeout=60.0) as client:
base, headers, sid = await ome_session(client)
try:
skip = 0
while True:
r = await client.get(
f"{base}/api/WarrantyService/Warranties",
headers=headers,
params={"$top": 200, "$skip": skip},
)
r.raise_for_status()
chunk = (r.json() or {}).get("value") or []
if not chunk:
break
for w in chunk:
rows.append(
{
"id": w.get("Id"),
"device_id": w.get("DeviceId"),
"service_tag": w.get("DeviceIdentifier"),
"device_name": w.get("DeviceName"),
"model": w.get("DeviceModel"),
"service_level": w.get("ServiceLevelDescription") or w.get("ServiceLevelCode"),
"start_date": w.get("StartDate"),
"end_date": w.get("EndDate"),
"days_remaining": w.get("DaysRemaining"),
"ship_date": w.get("SystemShipDate"),
"state": w.get("State"),
"country": w.get("CountryLookupCode"),
}
)
skip += len(chunk)
if len(chunk) < 200:
break
finally:
await ome_session_delete(client, base, headers, sid)
# Prefer longest-remaining warranty per device
by_device: dict[int, dict] = {}
for w in rows:
did = w.get("device_id")
if did is None:
continue
prev = by_device.get(did)
if not prev or int(w.get("days_remaining") or 0) > int(prev.get("days_remaining") or 0):
by_device[did] = w
elif int(w.get("days_remaining") or 0) == int(prev.get("days_remaining") or 0):
# keep later end date
if str(w.get("end_date") or "") > str(prev.get("end_date") or ""):
by_device[did] = w
payload = {
"updated_at": time.time(),
"count": len(rows),
"items": rows,
"by_device": {str(k): v for k, v in by_device.items()},
}
return _cache_set("warranties", payload)
async def fetch_baselines(force: bool = False) -> dict:
if not force:
cached = _cache_get("baselines")
if cached is not None:
return cached
async with httpx.AsyncClient(verify=False, timeout=60.0) as client:
base, headers, sid = await ome_session(client)
try:
r = await client.get(f"{base}/api/UpdateService/Baselines", headers=headers)
r.raise_for_status()
items = []
for b in (r.json() or {}).get("value") or []:
summary = b.get("ComplianceSummary") or {}
items.append(
{
"id": b.get("Id"),
"name": b.get("Name"),
"description": b.get("Description"),
"catalog_id": b.get("CatalogId"),
"catalog_type": b.get("CatalogType"),
"last_run": b.get("LastRun"),
"task_id": b.get("TaskId"),
"compliance_status": summary.get("ComplianceStatus"),
"critical": summary.get("NumberOfCritical"),
"warning": summary.get("NumberOfWarning"),
"normal": summary.get("NumberOfNormal"),
"downgrade": summary.get("NumberOfDowngrade"),
"unknown": summary.get("NumberOfUnknown"),
}
)
finally:
await ome_session_delete(client, base, headers, sid)
return _cache_set("baselines", {"updated_at": time.time(), "items": items})
async def fetch_catalogs(force: bool = False) -> dict:
if not force:
cached = _cache_get("catalogs")
if cached is not None:
return cached
async with httpx.AsyncClient(verify=False, timeout=60.0) as client:
base, headers, sid = await ome_session(client)
try:
r = await client.get(f"{base}/api/UpdateService/Catalogs", headers=headers)
r.raise_for_status()
items = []
for c in (r.json() or {}).get("value") or []:
repo = c.get("Repository") or {}
items.append(
{
"id": c.get("Id"),
"filename": c.get("Filename") or c.get("SourcePath"),
"source": repo.get("Source") or c.get("Source"),
"repository_name": repo.get("Name"),
"repository_type": repo.get("RepositoryType") or c.get("RepositoryType"),
"owner": c.get("Owner"),
"status": (c.get("Status") or {}).get("Name") if isinstance(c.get("Status"), dict) else c.get("Status"),
"last_updated": c.get("LastUpdated") or c.get("BundlesUpdateTime"),
}
)
finally:
await ome_session_delete(client, base, headers, sid)
return _cache_set("catalogs", {"updated_at": time.time(), "items": items})
def _pick_primary_baseline(baselines: list[dict]) -> dict | None:
if not baselines:
return None
for b in baselines:
name = (b.get("name") or "").lower()
if "dell" in name and "online" in name:
return b
# most critical first
return sorted(baselines, key=lambda x: int(x.get("critical") or 0), reverse=True)[0]
async def fetch_compliance(force: bool = False, baseline_id: int | None = None) -> dict:
if not force and baseline_id is None:
cached = _cache_get("compliance")
if cached is not None:
return cached
bl = await fetch_baselines(force=force)
primary = None
if baseline_id is not None:
primary = next((b for b in bl["items"] if b.get("id") == baseline_id), None)
if primary is None:
primary = _pick_primary_baseline(bl["items"])
if not primary:
payload = {
"updated_at": time.time(),
"summary": {"outdated_devices": 0, "critical_components": 0, "baseline_name": None},
"devices": [],
"components": [],
"baselines": bl["items"],
}
return _cache_set("compliance", payload)
bid = primary["id"]
devices_out: list[dict] = []
components_out: list[dict] = []
fleet_by_id = {d.get("id"): d for d in STATE.get("devices") or []}
async with httpx.AsyncClient(verify=False, timeout=120.0) as client:
base, headers, sid = await ome_session(client)
try:
r = await client.get(
f"{base}/api/UpdateService/Baselines({bid})/DeviceComplianceReports",
headers=headers,
)
r.raise_for_status()
for dcr in (r.json() or {}).get("value") or []:
did = dcr.get("DeviceId")
fleet = fleet_by_id.get(did) or {}
st = dcr.get("ServiceTag") or fleet.get("service_tag")
comps = dcr.get("ComponentComplianceReports") or []
device_row = {
"device_id": did,
"service_tag": st,
"name": fleet.get("name") or dcr.get("DeviceName") or st,
"model": dcr.get("DeviceModel") or fleet.get("model"),
"ip": fleet.get("ip"),
"firmware_status": dcr.get("FirmwareStatus"),
"compliance_status": dcr.get("ComplianceStatus"),
"reboot_required": dcr.get("RebootRequired"),
"component_count": len(comps),
"dell_uri": None,
}
devices_out.append(device_row)
for comp in comps:
uri = comp.get("Uri")
if uri and not device_row["dell_uri"]:
device_row["dell_uri"] = uri
components_out.append(
{
"device_id": did,
"service_tag": st,
"device_name": device_row["name"],
"model": device_row["model"],
"ip": device_row["ip"],
"component": comp.get("Name"),
"component_type": comp.get("ComponentType"),
"current_version": comp.get("CurrentVersion"),
"catalog_version": comp.get("Version"),
"update_action": comp.get("UpdateAction"),
"criticality": comp.get("Criticality"),
"compliance_status": comp.get("ComplianceStatus"),
"reboot_required": comp.get("RebootRequired"),
"dell_uri": uri,
"path": comp.get("Path"),
"baseline_id": bid,
"baseline_name": primary.get("name"),
"status_badge": (
"outdated"
if str(comp.get("UpdateAction") or "").upper() == "UPGRADE"
or str(comp.get("ComplianceStatus") or "").upper()
in ("CRITICAL", "WARNING", "NONCOMPLIANT", "NON-COMPLIANT")
else "current"
if str(comp.get("UpdateAction") or "").upper() in ("EQUAL", "NONE", "")
and str(comp.get("ComplianceStatus") or "").upper()
in ("", "OK", "COMPLIANT", "NORMAL", "DOWNGRADE")
else "unknown"
),
}
)
finally:
await ome_session_delete(client, base, headers, sid)
outdated = [
d
for d in devices_out
if str(d.get("compliance_status") or "").upper() in ("CRITICAL", "WARNING")
or str(d.get("firmware_status") or "").lower() in ("non-compliant", "noncompliant")
]
crit_comps = [
c
for c in components_out
if str(c.get("compliance_status") or "").upper() == "CRITICAL"
or str(c.get("update_action") or "").upper() == "UPGRADE"
]
payload = {
"updated_at": time.time(),
"baseline": primary,
"baselines": bl["items"],
"summary": {
"baseline_id": bid,
"baseline_name": primary.get("name"),
"outdated_devices": len(outdated),
"devices_in_report": len(devices_out),
"critical_components": len(crit_comps),
"components_total": len(components_out),
"compliance_status": primary.get("compliance_status"),
"last_run": primary.get("last_run"),
},
"devices": devices_out,
"components": components_out,
}
if baseline_id is None:
return _cache_set("compliance", payload)
return payload
async def fetch_report_defs(force: bool = False) -> dict:
if not force:
cached = _cache_get("report_defs")
if cached is not None:
return cached
items = []
async with httpx.AsyncClient(verify=False, timeout=60.0) as client:
base, headers, sid = await ome_session(client)
try:
r = await client.get(f"{base}/api/ReportService/ReportDefs", headers=headers)
r.raise_for_status()
for d in (r.json() or {}).get("value") or []:
cols = [c.get("Name") for c in (d.get("ColumnNames") or []) if c.get("Name")]
items.append(
{
"id": d.get("Id"),
"name": d.get("Name"),
"description": d.get("Description"),
"category": d.get("Category") or d.get("FilterGroupName"),
"is_builtin": d.get("IsBuiltIn"),
"last_run": d.get("LastRunDate"),
"last_run_by": d.get("LastRunBy"),
"columns": cols,
}
)
finally:
await ome_session_delete(client, base, headers, sid)
items.sort(key=lambda x: ((x.get("category") or ""), (x.get("name") or "").lower()))
return _cache_set("report_defs", {"updated_at": time.time(), "items": items})
async def fetch_jobs(force: bool = False, top: int = 40) -> dict:
if not force:
cached = _cache_get("jobs")
if cached is not None:
return cached
items = []
async with httpx.AsyncClient(verify=False, timeout=60.0) as client:
base, headers, sid = await ome_session(client)
try:
r = await client.get(
f"{base}/api/JobService/Jobs",
headers=headers,
params={"$top": top},
)
r.raise_for_status()
for j in (r.json() or {}).get("value") or []:
status = j.get("LastRunStatus") or {}
items.append(
{
"id": j.get("Id"),
"name": j.get("JobName") or j.get("Name"),
"status": status.get("Name") if isinstance(status, dict) else status,
"job_type": (j.get("JobType") or {}).get("Name")
if isinstance(j.get("JobType"), dict)
else j.get("JobType"),
"last_run": j.get("LastRunStatus") and (j.get("LastRunDate") or j.get("StartTime")),
"progress": j.get("Progress") or j.get("PercentComplete"),
"created_by": j.get("CreatedBy"),
}
)
finally:
await ome_session_delete(client, base, headers, sid)
return _cache_set("jobs", {"updated_at": time.time(), "items": items})
def build_fleet_report_rows(warranties: dict | None = None) -> list[dict]:
by_dev = (warranties or {}).get("by_device") or {}
rows = []
for d in STATE.get("devices") or []:
w = by_dev.get(str(d.get("id"))) or {}
rows.append(
{
"id": d.get("id"),
"name": d.get("name"),
"service_tag": d.get("service_tag"),
"model": d.get("model"),
"ip": d.get("ip"),
"subnet": d.get("subnet"),
"type": d.get("type"),
"sub_type": d.get("sub_type"),
"is_server": d.get("is_server"),
"is_idrac": d.get("is_idrac"),
"connected": d.get("connected"),
"powered_on": d.get("powered_on"),
"power_state": d.get("power_state"),
"status": d.get("status"),
"watts": d.get("watts"),
"avg_watts": d.get("avg_watts"),
"peak_watts": d.get("peak_watts"),
"last_status_time": d.get("last_status_time"),
"last_inventory_time": d.get("last_inventory_time"),
"warranty_end": w.get("end_date"),
"warranty_days_remaining": w.get("days_remaining"),
"warranty_service_level": w.get("service_level"),
}
)
rows.sort(key=lambda r: (r.get("name") or "").lower())
return rows
@app.get("/api/warranties")
async def api_warranties(force: bool = False):
return await fetch_warranties(force=force)
@app.get("/api/compliance")
async def api_compliance(force: bool = False, baseline_id: int | None = None):
return await fetch_compliance(force=force, baseline_id=baseline_id)
@app.get("/api/baselines")
async def api_baselines(force: bool = False):
return await fetch_baselines(force=force)
@app.get("/api/catalogs")
async def api_catalogs(force: bool = False):
return await fetch_catalogs(force=force)
@app.get("/api/ome/report-defs")
async def api_report_defs(force: bool = False):
return await fetch_report_defs(force=force)
@app.get("/api/ome/jobs")
async def api_ome_jobs(force: bool = False):
return await fetch_jobs(force=force)
class ReportRunIn(BaseModel):
report_def_id: int
@app.post("/api/ome/reports/run")
async def api_ome_report_run(payload: ReportRunIn):
async with httpx.AsyncClient(verify=False, timeout=60.0) as client:
base, headers, sid = await ome_session(client)
try:
r = await client.post(
f"{base}/api/ReportService/Actions/ReportService.RunReport",
headers=headers,
json={"ReportDefId": payload.report_def_id},
)
if r.status_code >= 400:
raise HTTPException(r.status_code, r.text[:500])
job_id = r.json() if isinstance(r.json(), (int, str)) else (r.json() or {}).get("Id") or r.text
return {
"job_id": job_id,
"report_def_id": payload.report_def_id,
"message": "Report job started in OME. Poll results shortly.",
"results_url": f"/api/ome/reports/{payload.report_def_id}/results",
}
finally:
await ome_session_delete(client, base, headers, sid)
@app.get("/api/ome/reports/{report_def_id}/results")
async def api_ome_report_results(report_def_id: int):
async with httpx.AsyncClient(verify=False, timeout=60.0) as client:
base, headers, sid = await ome_session(client)
try:
r = await client.get(
f"{base}/api/ReportService/ReportDefs({report_def_id})/ReportResults",
headers=headers,
)
if r.status_code >= 400:
raise HTTPException(
r.status_code,
(r.json().get("error", {}) or {}).get("message")
if r.headers.get("content-type", "").startswith("application/json")
else r.text[:500],
)
return r.json()
finally:
await ome_session_delete(client, base, headers, sid)
@app.get("/api/reports/fleet")
async def api_reports_fleet(force: bool = False):
warranties = await fetch_warranties(force=force)
rows = build_fleet_report_rows(warranties)
return {
"updated_at": STATE.get("updated_at"),
"summary": STATE.get("summary"),
"ome": STATE.get("ome"),
"count": len(rows),
"rows": rows,
}
@app.get("/api/reports/firmware")
async def api_reports_firmware(force: bool = False, baseline_id: int | None = None):
return await fetch_compliance(force=force, baseline_id=baseline_id)
@app.get("/api/reports/brief")
async def api_reports_brief(force: bool = False):
warranties = await fetch_warranties(force=force)
compliance = await fetch_compliance(force=force)
fleet_rows = build_fleet_report_rows(warranties)
critical = [a for a in (STATE.get("alerts") or []) if a.get("severity") == "Critical"][:15]
expiring = sorted(
[
w
for w in (warranties.get("items") or [])
if w.get("days_remaining") is not None and int(w.get("days_remaining") or 0) <= 90
],
key=lambda x: int(x.get("days_remaining") or 0),
)[:25]
outdated = [
d
for d in (compliance.get("devices") or [])
if str(d.get("compliance_status") or "").upper() in ("CRITICAL", "WARNING")
or str(d.get("firmware_status") or "").lower() in ("non-compliant", "noncompliant")
]
return {
"generated_at": time.time(),
"ome": STATE.get("ome"),
"summary": STATE.get("summary"),
"compliance_summary": compliance.get("summary"),
"critical_alerts": critical,
"outdated_devices": outdated[:40],
"warranty_expiring": expiring,
"fleet": fleet_rows,
"baseline": compliance.get("baseline"),
}
@app.get("/api/export/{kind}")
async def api_export(kind: str, fmt: str = "csv", force: bool = False, baseline_id: int | None = None):
kind = kind.lower().strip()
fmt = fmt.lower().strip()
if fmt not in ("csv", "json"):
raise HTTPException(400, "fmt must be csv or json")
if kind in ("fleet", "inventory"):
data = await api_reports_fleet(force=force)
rows = data["rows"]
filename = f"ome-fleet-{int(time.time())}"
elif kind in ("firmware", "compliance"):
data = await fetch_compliance(force=force, baseline_id=baseline_id)
rows = data.get("components") or []
filename = f"ome-firmware-compliance-{int(time.time())}"
elif kind == "warranty":
data = await fetch_warranties(force=force)
rows = data.get("items") or []
filename = f"ome-warranties-{int(time.time())}"
elif kind == "brief":
data = await api_reports_brief(force=force)
if fmt == "json":
return Response(
content=json.dumps(data, indent=2, default=str),
media_type="application/json",
headers={"Content-Disposition": f'attachment; filename="ome-customer-brief-{int(time.time())}.json"'},
)
# flatten brief as fleet CSV appendix
rows = data.get("fleet") or []
filename = f"ome-customer-brief-fleet-{int(time.time())}"
else:
raise HTTPException(404, "Unknown export kind. Use fleet|firmware|warranty|brief")
if fmt == "json":
body = json.dumps({"kind": kind, "count": len(rows), "rows": rows}, indent=2, default=str)
return Response(
content=body,
media_type="application/json",
headers={"Content-Disposition": f'attachment; filename="{filename}.json"'},
)
csv_body = rows_to_csv(rows)
return Response(
content=csv_body,
media_type="text/csv",
headers={"Content-Disposition": f'attachment; filename="{filename}.csv"'},
)
@app.get("/api/devices/{device_id}/warranty")
async def device_warranty(device_id: int, force: bool = False):
warranties = await fetch_warranties(force=force)
items = [w for w in warranties.get("items") or [] if w.get("device_id") == device_id]
primary = (warranties.get("by_device") or {}).get(str(device_id))
return {"device_id": device_id, "primary": primary, "items": items}
@app.get("/api/devices/{device_id}/compliance")
async def device_compliance(device_id: int, force: bool = False):
compliance = await fetch_compliance(force=force)
device = next((d for d in compliance.get("devices") or [] if d.get("device_id") == device_id), None)
comps = [c for c in compliance.get("components") or [] if c.get("device_id") == device_id]
return {
"device_id": device_id,
"baseline": compliance.get("baseline"),
"device": device,
"components": comps,
}
@app.get("/api/ssh/sessions")
async def api_ssh_sessions():
"""Ops visibility: active SSH bridges (no secrets)."""
@@ -1561,6 +2259,11 @@ async def vendor_xterm_fit_js():
)
@app.get("/reports.js")
async def reports_js():
return FileResponse(STATIC_DIR / "reports.js", media_type="application/javascript")
@app.get("/dell.png")
async def dell_png():
return FileResponse(STATIC_DIR / "dell.png", media_type="image/png")