diff --git a/docs/PROJECT-MAP.md b/docs/PROJECT-MAP.md index 3cf2ef68..aea4bad9 100644 --- a/docs/PROJECT-MAP.md +++ b/docs/PROJECT-MAP.md @@ -91,5 +91,5 @@ Manifest-less directories under `plugins/` are core frontend surface and always ## Size - test functions defined: **1449** (parametrised cases collect higher) -- documented API paths: **332** (`docs/openapi.json`, regenerate with `scripts/gen_openapi.py`) +- documented API paths: **333** (`docs/openapi.json`, regenerate with `scripts/gen_openapi.py`) diff --git a/docs/api-inventory.json b/docs/api-inventory.json index 2254f0c4..b52a6143 100644 --- a/docs/api-inventory.json +++ b/docs/api-inventory.json @@ -1857,6 +1857,14 @@ "params": "path: asset_id", "purpose": "Preview the resolved effective policy for one endpoint: which Business Policy won, which Configuration Policies matched, and the resulting desired/gate actions. Read-only, for debugging a scope that is not matching what its author expected; 404 while the computers_configpolicies_enabled setting is off", "example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/computers/policies/effective/1asset_id" + }, + { + "method": "GET", + "path": "/api/computers/asset//policypanel", + "auth": "computers.view", + "params": "path: asset_id", + "purpose": "Asset-detail panel: which policy governs this endpoint, how many configuration policies folded into it, and how many actions it allows or drives to a desired state. Provenance rather than a control - the effective policy narrows the command allowlist, so this is the answer to why an action is unavailable on a given PC. Returns an empty field list (not the feature-gate 404) when the policy feature is off, so the card self-hides instead of erroring on every site that has not enabled it", + "example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/computers/asset/1/policypanel" } ], "surface": "plugin-computers" diff --git a/docs/openapi.json b/docs/openapi.json index b43fc9e2..b72ab67c 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -10820,6 +10820,57 @@ ] } }, + "/api/computers/asset/{asset_id}/policypanel": { + "get": { + "tags": [ + "plugin-computers" + ], + "summary": "Asset-detail panel: which policy governs this endpoint, how many configuration policies folded into it, and how many...", + "description": "Asset-detail panel: which policy governs this endpoint, how many configuration policies folded into it, and how many actions it allows or drives to a desired state. Provenance rather than a control - the effective policy narrows the command allowlist, so this is the answer to why an action is unavailable on a given PC. Returns an empty field list (not the feature-gate 404) when the policy feature is off, so the card self-hides instead of erroring on every site that has not enabled it\n\n**Auth:** computers.view\n\n**Params:** path: asset_id\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/computers/asset/1/policypanel\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "asset_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, "/api/measuringtools/types": { "get": { "tags": [ diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js index 4ad32277..bc38bba6 100644 --- a/frontend/src/api/index.js +++ b/frontend/src/api/index.js @@ -159,6 +159,9 @@ export const endpointsApi = { dashboardSummary() { return api.get('/endpoints/dashboard/summary') }, + dashboardStability() { + return api.get('/endpoints/dashboard/stability') + }, list(params = {}) { return api.get('/endpoints', { params }) }, diff --git a/frontend/src/components/AssetTriageBand.vue b/frontend/src/components/AssetTriageBand.vue index 1e85040e..270fc1c4 100644 --- a/frontend/src/components/AssetTriageBand.vue +++ b/frontend/src/components/AssetTriageBand.vue @@ -36,6 +36,15 @@ Location {{ locationname }} + + +
+ +
@@ -52,7 +61,7 @@ * because a machine stops without it, and that fact was buried in the generic * relationships card near the bottom of the page. */ -import { ref, computed, onMounted, watch } from 'vue' +import { ref, computed, onMounted, watch, useSlots } from 'vue' import api from '@/api' const props = defineProps({ @@ -67,7 +76,10 @@ const props = defineProps({ const machines = ref([]) +const slots = useSlots() + const hasAnything = computed(() => + Boolean(slots.actions) || Boolean(props.liveness) || machines.value.length > 0 || (props.accessmethods && props.accessmethods.length > 0) || @@ -122,6 +134,14 @@ function collect(edge, other, found) { gap: 0.3rem; } +.triage-actions { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + align-items: center; + margin-left: auto; +} + .triage-label { font-size: 0.72rem; letter-spacing: 0.06em; diff --git a/plugins/computers/api/policy_routes.py b/plugins/computers/api/policy_routes.py index e8e749be..a515acc8 100644 --- a/plugins/computers/api/policy_routes.py +++ b/plugins/computers/api/policy_routes.py @@ -397,3 +397,51 @@ def get_effective_policy(asset_id: int): from ..services.policyresolver import effective_policy_for effective = effective_policy_for(asset_id) return success_response(effective.to_dict()) + + +@computers_bp.route('/asset//policypanel', methods=['GET']) +@require_permission('computers.view') +def policy_panel(asset_id: int): + """Keyvalue panel: which policy governs this endpoint, and what it allows. + + Provenance, not a control. It matters because the effective policy already + narrows the command allowlist, so it is the answer to "why is Start greyed + out on this PC" - which is otherwise invisible. + + Returns an EMPTY field list rather than the feature-gate 404 when the + feature is off. The panel contract hides a card with no fields; an error + would render a broken card on every site that has not enabled policies, + which is most of them. + """ + if not policy_flag_enabled(): + return success_response({'fields': []}) + + from ..services.policyresolver import effective_policy_for + effective = effective_policy_for(asset_id) + actions = effective.to_dict() + if not actions: + return success_response({'fields': []}) + + # One endpoint resolves through one winning business policy, but read the + # ids off the actions rather than assuming: a fold can draw from more than + # one configuration policy. + businesspolicyids = {a['businesspolicyid'] for a in actions.values() + if a.get('businesspolicyid')} + configpolicyids = {a['configurationpolicyid'] for a in actions.values() + if a.get('configurationpolicyid')} + + names = [] + if businesspolicyids: + names = [p.name for p in BusinessPolicy.query + .filter(BusinessPolicy.businesspolicyid.in_(businesspolicyids)).all()] + + gated = sum(1 for a in actions.values() if a.get('mode') == ACTION_MODE_GATE) + desired = sum(1 for a in actions.values() if a.get('mode') == ACTION_MODE_DESIRED) + + fields = [] + if names: + fields.append({'label': 'Policy', 'value': ', '.join(sorted(names))}) + fields.append({'label': 'Configuration policies', 'value': str(len(configpolicyids))}) + fields.append({'label': 'Allowed actions', 'value': str(gated)}) + fields.append({'label': 'Desired state', 'value': str(desired)}) + return success_response({'fields': fields}) diff --git a/plugins/computers/frontend/views/PCDetail.vue b/plugins/computers/frontend/views/PCDetail.vue index 45cd3647..3edc8625 100644 --- a/plugins/computers/frontend/views/PCDetail.vue +++ b/plugins/computers/frontend/views/PCDetail.vue @@ -2,9 +2,12 @@
diff --git a/plugins/computers/plugin.py b/plugins/computers/plugin.py index c724fa54..0b3c10cb 100644 --- a/plugins/computers/plugin.py +++ b/plugins/computers/plugin.py @@ -1967,6 +1967,29 @@ class ComputersPlugin(BasePlugin): }, ] + def get_asset_panels(self) -> List[Dict]: + """Policy provenance on the PC detail page. + + The effective policy already narrows which commands an endpoint will + accept, so without this the reason a control is unavailable is not + visible anywhere. Right column, near the services it governs: this is + provenance, not a 6am triage signal. + + The route returns an empty field list when the policy feature is off, + so the card disappears on sites that have not enabled it rather than + rendering an error. + """ + return [ + { + 'id': 'computers-policy', + 'title': 'Policy', + 'assettypes': ['computer'], + 'endpoint': '/api/computers/asset/{assetid}/policypanel', + 'render': 'keyvalue', + 'position': 40, + }, + ] + def get_navigation_items(self) -> List[Dict]: """Return navigation menu items.""" return [ diff --git a/plugins/endpoints/frontend/views/EndpointsList.vue b/plugins/endpoints/frontend/views/EndpointsList.vue index 6cf87bf1..22f08e81 100644 --- a/plugins/endpoints/frontend/views/EndpointsList.vue +++ b/plugins/endpoints/frontend/views/EndpointsList.vue @@ -7,7 +7,7 @@
- + +
@@ -36,6 +41,18 @@ + +