Skip to content

Commit a971df6

Browse files
committed
refactor: retire the inventory app's browser surface and mothball system-facts collection
Stop advertising a page no deployment can render: sidebar=False on the app definition, SIDEBAR: false on the embedded profile, and the shell's hardcoded Inventory nav row deleted. Drop SystemFactsSyncer from the embedded profile's SYNCERS list — configuration only, so a settings override restores collection. Remove the five orphaned SEP routes under /api/apps/inventory/ (entity list and detail, both system-observation proxies, and the app schema), the AppSchema itself, and the deps and list-query helpers those routes were the last callers of. Empty the React package's rendered surface down to a husk that the shell's lazy import still resolves, and drop the dashboard's Nodes and Targets links into it — both cards keep their counts. The syncer code, observation models, inventory-service endpoints and already collected rows are untouched.
1 parent a157c14 commit a971df6

59 files changed

Lines changed: 244 additions & 11555 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/labeler.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,6 @@ app:inventory:
104104
- 'app/sep/apps/inventory/**'
105105
- 'frontend/packages/apps/inventory/**'
106106
- 'tests/app/sep/apps/inventory/**'
107-
- 'frontend/packages/e2e/tests/inventory*.spec.ts'
108107
app:mysql_backups:
109108
- any:
110109
- changed-files:

app/sep/apps/inventory/api_routes.py

Lines changed: 18 additions & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -16,76 +16,47 @@
1616
"""Define the JSON API router for the Inventory plugin.
1717
1818
Mounted at ``/api/apps/inventory/`` via ``apps_router`` in
19-
``app/sep/api/router.py``. Like other plugin proxies, these routes rely on the
20-
parent ``api_router`` for API authentication. The ``schema_endpoint`` helper
21-
additionally attaches ``IsApiAuthenticated`` to the schema route only; the list
22-
and detail handlers do not duplicate that dependency.
23-
24-
Proxies read access to nodes, services, schemas, and tables to the inventory
25-
HTTP API through ``InventoryAPI`` in ``app.sep.deps`` (``RemoteAPI`` toward the
26-
inventory service). List handlers unwrap paginated ``items`` into a JSON array
27-
for the schema-driven React client. The four entities are read-only here: no
28-
handler creates, updates, or deletes one. The syncers author that data — PMM
29-
supplies nodes and services, while ``MySQLSyncer`` discovers schemas and tables
30-
from the database itself — and they write through the inventory service, which
31-
remains the canonical CRUD surface at ``/api/inventory/*``.
32-
33-
Besides those reads, this router mounts the ad-hoc inventory-sync trigger
34-
(``POST /sync/``) and the running-state polling endpoint
35-
(``GET /sync/status/``) consumed by the React inventory sync control. Schedule
36-
discovery (``GET /``) and available-syncers (``GET /available-syncers/``) are
37-
also mounted here so the React schedule UI can fetch its data
38-
through the plugin API gateway. Periodic-task CRUD remains delegated to
19+
``app/sep/api/router.py``, which supplies the API authentication these routes
20+
rely on.
21+
22+
This is an operator API only — the plugin ships no browser surface, so nothing
23+
here proxies entity reads. Read the catalog from the inventory service itself
24+
at ``/api/inventory/*``, which remains its canonical CRUD surface and the one
25+
the syncers write through.
26+
27+
The router mounts the ad-hoc inventory-sync trigger (``POST /sync/``), the
28+
running-state polling endpoint (``GET /sync/status/``), schedule discovery
29+
(``GET /``), available-syncers (``GET /available-syncers/``), and the
30+
per-service connectivity probe. Periodic-task CRUD remains delegated to
3931
``/api/tasks/periodic/*`` as the single source of truth; this router does not
4032
duplicate that surface.
4133
"""
4234

4335
from __future__ import annotations
4436

45-
from typing import Any
46-
47-
from fastapi import APIRouter, BackgroundTasks, Body, Request, Response, status
37+
from fastapi import APIRouter, BackgroundTasks, Body, Response, status
4838
from sqlmodel import col
4939

5040
from app.core.exceptions import HTTPBadRequestException
51-
from app.core.pagination import (
52-
build_proxied_page,
53-
PaginatedResponse,
54-
PaginationDep,
55-
)
56-
from app.sep.apps.framework.api import schema_endpoint
5741
from app.sep.apps.inventory.connectivity import probe_service_connectivity
5842
from app.sep.apps.inventory.deps import (
5943
AvailableSyncer,
6044
filter_syncers_by_name,
6145
InternalTokenDep,
62-
inventory_plugin_query_params,
63-
inventory_service_detail_path,
64-
inventory_service_list_path,
65-
inventory_system_observation_path,
6646
InventoryAvailableSyncersDep,
6747
InventorySyncStatusResponse,
6848
InventorySyncTriggerWrite,
69-
require_inventory_plugin_entity,
7049
SyncersDep,
71-
SYSTEM_OBSERVATION_SEGMENT,
72-
unwrap_inventory_plugin_list_payload,
73-
)
74-
from app.sep.apps.inventory.list_query import (
75-
InventoryListQueryDep,
76-
list_query_upstream_params,
7750
)
7851
from app.sep.apps.inventory.models import (
7952
INVENTORY_SYNC_TASK_NAME,
8053
PluginTaskResponse,
8154
SyncRunSummary,
8255
)
83-
from app.sep.apps.inventory.schema import inventory_schema
8456
from app.sep.apps.inventory.sync import run_inventory_sync
8557
from app.sep.crud import SyncInstanceManager, SyncItemManager
8658
from app.sep.deps import (
8759
CreatedServiceDep,
88-
InventoryAPI,
8960
IsApiAdmin,
9061
SessionDep,
9162
TaskAPI,
@@ -95,7 +66,6 @@
9566
from app.tasks.models import INVENTORY_COLLECTION_TASK_NAME
9667

9768
router = APIRouter()
98-
schema_endpoint(router=router, plugin_schema=inventory_schema)
9969

10070
# Module-level singleton avoids the B008 lint warning about function calls in
10171
# argument defaults; the optional-body semantics are unchanged.
@@ -151,9 +121,8 @@ async def inventory_sync_trigger(
151121
async def inventory_sync_status(session: SessionDep) -> InventorySyncStatusResponse:
152122
"""Return whether an inventory-wide sync is running, plus recent run outcomes.
153123
154-
Replaces the server-rendered ``sync_is_running`` template variable
155-
used by the Jinja2 inventory page so the React control can poll the
156-
same state without scraping HTML.
124+
Lets an operator poll a sync they triggered through ``POST /sync/``
125+
without scraping any rendered page.
157126
158127
:param session: SQLModel async session.
159128
:return: The running flag and the most recent runs, newest first.
@@ -190,9 +159,7 @@ async def inventory_plugin_tasks() -> list[PluginTaskResponse]:
190159
"""Return the list of periodic task names for the Inventory plugin.
191160
192161
Hard-coded because the Inventory plugin's periodic tasks are a fixed pair
193-
(``inventory-sync`` and ``inventory-collection``). The shape matches what the
194-
React ``usePluginTasks('inventory')`` hook expects: a list of objects with at
195-
minimum a ``name`` key.
162+
(``inventory-sync`` and ``inventory-collection``).
196163
197164
:return: The plugin's periodic tasks, each with its name and display name.
198165
"""
@@ -221,82 +188,6 @@ async def inventory_available_syncers(
221188
return available_syncers
222189

223190

224-
@router.get("/{entity}/")
225-
async def inventory_list_entity(
226-
request: Request,
227-
entity: str,
228-
inventory_api: InventoryAPI,
229-
pagination: PaginationDep,
230-
list_query: InventoryListQueryDep,
231-
) -> PaginatedResponse[Any]:
232-
"""List inventory nodes, services, schemas, or tables.
233-
234-
:param request: Inbound request; its query string carries entity filters.
235-
:param entity: Inventory entity type (nodes, services, schemas, tables).
236-
:param inventory_api: Async client for the Inventory sub-app.
237-
:param pagination: Validated offset/limit forwarded to the upstream call.
238-
:param list_query: Allowlist-vetted sort/search for this entity.
239-
:return: A paginated envelope echoing the requested window.
240-
"""
241-
entity = require_inventory_plugin_entity(entity)
242-
params = inventory_plugin_query_params(request)
243-
params["offset"] = pagination.offset
244-
params["limit"] = pagination.limit
245-
# Drop raw sort/search before merging the validated adapter output so a
246-
# blank or omitted search cannot leak through from the query string.
247-
params.pop("sort", None)
248-
params.pop("search", None)
249-
params.update(list_query_upstream_params(list_query))
250-
data = await inventory_api.get(inventory_service_list_path(entity), params=params)
251-
items = unwrap_inventory_plugin_list_payload(data)
252-
envelope = data if isinstance(data, dict) else {}
253-
return build_proxied_page(items, envelope, pagination, client_side_filtered=False)
254-
255-
256-
@router.get(f"/nodes/{{node_id:int}}/{SYSTEM_OBSERVATION_SEGMENT}")
257-
async def inventory_node_system_observation(
258-
node_id: int,
259-
inventory_api: InventoryAPI,
260-
) -> Any:
261-
"""Proxy the host-level system observation for a node (read-only).
262-
263-
Forwards to the inventory sub-app's ``/nodes/{node_id}/system-observation``
264-
endpoint via ``InventoryAPI``. This three-segment literal path cannot
265-
collide with the two-segment ``/{entity}/{item_id:int}`` detail matcher. An
266-
upstream HTTP 404 propagates unchanged, along with the ``detail`` that tells
267-
a node whose observation has not been collected yet — which the React panel
268-
renders as an empty state — apart from a node that does not exist.
269-
270-
:param node_id: Primary key of the node.
271-
:param inventory_api: Authenticated inventory ``RemoteAPI`` client.
272-
:return: The host-level system observation payload.
273-
"""
274-
return await inventory_api.get(inventory_system_observation_path("nodes", node_id))
275-
276-
277-
@router.get(f"/services/{{service_id:int}}/{SYSTEM_OBSERVATION_SEGMENT}")
278-
async def inventory_service_system_observation(
279-
service_id: int,
280-
inventory_api: InventoryAPI,
281-
) -> Any:
282-
"""Proxy the service-level system observation for a service (read-only).
283-
284-
Forwards to the inventory sub-app's
285-
``/services/{service_id}/system-observation`` endpoint via ``InventoryAPI``.
286-
An upstream HTTP 404 propagates unchanged, along with the ``detail`` that
287-
tells a service whose observation has not been collected yet — which the
288-
React panel renders as an empty state — apart from a service that does not
289-
exist.
290-
291-
:param service_id: Primary key of the service.
292-
:param inventory_api: Authenticated inventory ``RemoteAPI`` client.
293-
:return: The service-level system observation payload.
294-
"""
295-
return await inventory_api.get(
296-
inventory_system_observation_path("services", service_id)
297-
)
298-
299-
300191
@router.post(
301192
"/services/{service_id:int}/check-connectivity/",
302193
dependencies=[IsApiAdmin],
@@ -307,12 +198,9 @@ async def inventory_service_check_connectivity(
307198
) -> ConnectivityCheckResponse:
308199
"""Run a database connectivity probe for a service from its executor host.
309200
310-
Backs the React connectivity control on the service detail page. A probe
311-
that ran but could not connect is reported as HTTP 200 with
201+
A probe that ran but could not connect is reported as HTTP 200 with
312202
``success=false`` and the upstream message in ``error``; only a probe that
313-
could not be attempted at all is an error status. This three-segment
314-
literal path cannot collide with the two-segment
315-
``/{entity}/{item_id:int}`` detail matcher.
203+
could not be attempted at all is an error status.
316204
317205
:param service: The service to probe, resolved from the path id.
318206
:param tasks_api: Authenticated Tasks ``RemoteAPI`` client.
@@ -324,14 +212,3 @@ async def inventory_service_check_connectivity(
324212
returns an unparseable body.
325213
"""
326214
return await probe_service_connectivity(service, tasks_api)
327-
328-
329-
@router.get("/{entity}/{item_id:int}")
330-
async def inventory_get_entity(
331-
entity: str,
332-
item_id: int,
333-
inventory_api: InventoryAPI,
334-
) -> Any:
335-
"""Retrieve a single inventory node, service, schema, or table."""
336-
entity = require_inventory_plugin_entity(entity)
337-
return await inventory_api.get(inventory_service_detail_path(entity, item_id))

app/sep/apps/inventory/app.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,16 @@
1616
"""Wire the Inventory plugin as a declarative ``BaseApp``.
1717
1818
Register the bespoke inventory plugin through the registry's definition path
19-
instead of the synthesized-legacy fallback, carrying its existing
20-
``inventory_schema`` so the conformance suite reads the schema from the
21-
definition rather than the live ``GET /schema`` endpoint.
19+
instead of the synthesized-legacy fallback. The app ships no browser surface:
20+
it declares no ``AppSchema`` and stays out of the sidebar, exposing only the
21+
operator API under ``/api/apps/inventory/``.
2222
"""
2323

2424
import json
2525

2626
from app.sep.apps.framework.base import AppPeriodicTask, BaseApp
2727
from app.sep.apps.inventory.api_routes import router as api_router
2828
from app.sep.apps.inventory.config import inventory_app_settings
29-
from app.sep.apps.inventory.schema import inventory_schema
3029
from app.tasks.models import INVENTORY_COLLECTION_TASK_NAME
3130

3231
COLLECTION_SCHEDULE_NAME = "sep__inventory_collection"
@@ -66,6 +65,6 @@ def _collection_task() -> AppPeriodicTask:
6665
uri_path="/inventory",
6766
css_class="inventory",
6867
api_router=api_router,
69-
schema=inventory_schema,
68+
sidebar=False,
7069
periodic_task_schedules=[_collection_task()],
7170
)

app/sep/apps/inventory/deps.py

Lines changed: 1 addition & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,10 @@
1818
from collections.abc import Callable
1919
from typing import Annotated, Any
2020

21-
from fastapi import Depends, Request
21+
from fastapi import Depends
2222
from pydantic import BaseModel, ConfigDict
2323

2424
from app.core.config import settings
25-
from app.core.exceptions import (
26-
HTTPBadGatewayException,
27-
HTTPNotFoundException,
28-
)
2925
from app.core.requests import RemoteAPI
3026
from app.core.security import require_internal_token
3127
from app.core.utils import import_var
@@ -36,13 +32,6 @@
3632
from app.sep.sync.models import BaseSyncer
3733
from app.tasks.config import tasks_settings
3834

39-
INVENTORY_PLUGIN_ENTITY_NAMES = frozenset({"nodes", "services", "schemas", "tables"})
40-
41-
# Single source of truth for the read-only system-observation sub-resource
42-
# segment, shared by the proxy route decorators and the forwarded-path helper
43-
# so the inbound and forwarded paths cannot drift apart.
44-
SYSTEM_OBSERVATION_SEGMENT = "system-observation"
45-
4635

4736
class InventorySyncTriggerWrite(BaseModel):
4837
"""Carry the optional JSON body for the ad-hoc inventory sync trigger.
@@ -313,96 +302,4 @@ async def get_syncers_standalone() -> list[BaseSyncer]:
313302
return syncers
314303

315304

316-
def require_inventory_plugin_entity(entity: str) -> str:
317-
"""Normalize ``entity`` or raise HTTP 404 when it is not a known segment.
318-
319-
:param entity: URL segment under ``/api/apps/inventory/``.
320-
:type entity: str
321-
:return: The same value when it is one of ``nodes``, ``services``,
322-
``schemas``, or ``tables``.
323-
:rtype: str
324-
:raises HTTPNotFoundException: When ``entity`` is unknown.
325-
"""
326-
if entity not in INVENTORY_PLUGIN_ENTITY_NAMES:
327-
raise HTTPNotFoundException("Unknown entity")
328-
return entity
329-
330-
331-
def unwrap_inventory_plugin_list_payload(data: Any) -> list[Any]:
332-
"""Return a list from an inventory list response (paginated or bare array).
333-
334-
:param data: JSON payload from the inventory list endpoint.
335-
:type data: Any
336-
:return: ``data["items"]`` when that key holds a list; otherwise ``data``
337-
when it is already a list.
338-
:rtype: list[Any]
339-
:raises HTTPBadGatewayException: When the payload shape is unexpected.
340-
"""
341-
if isinstance(data, dict) and "items" in data:
342-
items = data["items"]
343-
if isinstance(items, list):
344-
return items
345-
if isinstance(data, list):
346-
return data
347-
raise HTTPBadGatewayException("Unexpected inventory list response shape")
348-
349-
350-
def inventory_service_list_path(entity: str) -> str:
351-
"""Map a plugin entity name to the inventory service path for list GET.
352-
353-
:param entity: One of ``nodes``, ``services``, ``schemas``, or ``tables``.
354-
:type entity: str
355-
:return: Path relative to the inventory API root (``/nodes/`` for nodes).
356-
:rtype: str
357-
"""
358-
if entity == "nodes":
359-
return "/nodes/"
360-
return f"/{entity}/"
361-
362-
363-
def inventory_service_detail_path(entity: str, item_id: int) -> str:
364-
"""Map a plugin entity and id to the inventory path for detail GET/PUT/DELETE.
365-
366-
:param entity: One of ``nodes``, ``services``, ``schemas``, or ``tables``.
367-
:type entity: str
368-
:param item_id: Primary key of the row.
369-
:type item_id: int
370-
:return: Path relative to the inventory API root.
371-
:rtype: str
372-
"""
373-
if entity == "nodes":
374-
return f"/nodes/{item_id}"
375-
return f"/{entity}/{item_id}"
376-
377-
378-
def inventory_system_observation_path(entity: str, item_id: int) -> str:
379-
"""Map a plugin entity and id to the inventory system-observation sub-resource.
380-
381-
Built by appending ``/system-observation`` to the detail path from
382-
``inventory_service_detail_path`` so the sub-resource always tracks the
383-
canonical detail mapping and the two cannot drift. Targets the read-only
384-
system-observation endpoint exposed by the inventory sub-app. Only
385-
``nodes`` and ``services`` carry an observation; callers reach this helper
386-
through the explicit per-entity proxy routes.
387-
388-
:param entity: ``nodes`` or ``services``.
389-
:param item_id: Primary key of the node or service.
390-
:return: Path relative to the inventory API root.
391-
"""
392-
return (
393-
f"{inventory_service_detail_path(entity, item_id)}/{SYSTEM_OBSERVATION_SEGMENT}"
394-
)
395-
396-
397-
def inventory_plugin_query_params(request: Request) -> dict[str, Any]:
398-
"""Collect non-empty query string parameters from ``request``.
399-
400-
:param request: The inbound HTTP request.
401-
:type request: Request
402-
:return: Key/value pairs with empty string values omitted.
403-
:rtype: dict[str, Any]
404-
"""
405-
return {k: v for k, v in request.query_params.items() if v is not None and v != ""}
406-
407-
408305
InternalTokenDep = Annotated[str, Depends(require_internal_token)]

0 commit comments

Comments
 (0)