Import the wilm endpoint-monitor and channel-distribution work
Everything on origin/wilm that is not on the published main, applied as one
change because the published history has rewritten hashes: per-commit
cherry-pick conflicts against our main even though the squashed diff applies
clean.
Contents: endpoint monitor catalog + policy attachment + warn-only drift
reconciler, operator issue reports from the tray, agent launch-path allowlist,
and the GHCR channel-image distribution path for dev workstations.
NOT READY FOR main. Open items:
- 10 new routes are absent from docs/api-inventory.json, so
test_api_inventory_parity fails. That gate SKIPS on the published mirror
(the test ships, its data file is scrubbed), which is why wilm never saw it.
- dev-bootstrap.ps1 and Start-ShopDBDev.ps1 hardcode the ge-aero GitHub org.
- plugins/endpoints/plugin.py carries a whitespace-only re-indent.
Scope: product
This commit is contained in:
@@ -25,7 +25,7 @@
|
||||
<td class="cell-truncate" :title="p.description">{{ p.description || '-' }}</td>
|
||||
<td>
|
||||
<span v-for="a in p.actions" :key="a.action" class="badge" style="margin-right:4px;">
|
||||
{{ a.action }} ({{ a.mode }})
|
||||
{{ actionLabel(a.action) }} ({{ a.mode }})
|
||||
</span>
|
||||
<span v-if="!p.actions || p.actions.length === 0" style="color: var(--text-light);">none</span>
|
||||
</td>
|
||||
@@ -74,17 +74,23 @@
|
||||
<div class="form-group">
|
||||
<label>Actions</label>
|
||||
<div v-for="(a, index) in form.actions" :key="index" class="action-row">
|
||||
<select v-model="a.action" class="form-control">
|
||||
<option :value="''" disabled>Select action...</option>
|
||||
<option v-for="opt in actionCatalog" :key="opt.action" :value="opt.action">
|
||||
{{ opt.label || opt.action }}
|
||||
</option>
|
||||
</select>
|
||||
<ActionPicker
|
||||
v-model="a.action"
|
||||
:command-actions="actionCatalog"
|
||||
:monitors="monitors"
|
||||
@monitor-created="onMonitorCreated"
|
||||
/>
|
||||
<select v-model="a.mode" class="form-control mode-select">
|
||||
<option value="gate">Allow (gate)</option>
|
||||
<option value="desired">Desired state</option>
|
||||
</select>
|
||||
<button type="button" class="btn btn-danger btn-sm" @click="form.actions.splice(index, 1)">Remove</button>
|
||||
<p v-if="showDesiredWarning(a)" class="desired-warning">
|
||||
Desired state keeps this app alive: on agent check-in the
|
||||
server will try to START the process if a launch path is
|
||||
mapped. Use gate to only report status with Start/Stop in the
|
||||
Monitor UI.
|
||||
</p>
|
||||
</div>
|
||||
<button type="button" class="btn btn-secondary btn-sm" @click="addAction">+ Add action</button>
|
||||
</div>
|
||||
@@ -103,10 +109,11 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { computersApi, applicationsApi } from '@/api'
|
||||
import { computersApi, applicationsApi, endpointsApi } from '@/api'
|
||||
import { useToast } from '@/composables/toast'
|
||||
import { apiError } from '@/utils/apiError'
|
||||
import ScopeNodeEditor from './ScopeNodeEditor.vue'
|
||||
import ActionPicker from './ActionPicker.vue'
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
@@ -114,6 +121,7 @@ const policies = ref([])
|
||||
const pctypes = ref([])
|
||||
const apps = ref([])
|
||||
const actionCatalog = ref([])
|
||||
const monitors = ref([])
|
||||
const loading = ref(true)
|
||||
|
||||
const showModal = ref(false)
|
||||
@@ -132,21 +140,60 @@ onMounted(() => loadData())
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const [pol, types, appList, actions] = await Promise.all([
|
||||
// allSettled, not all: one failing endpoint must not blank the others.
|
||||
// A rejected Promise.all left PC types and apps empty when only monitors
|
||||
// 500'd, which looked like data loss.
|
||||
const [pol, types, appList, actions, monitorList] = await Promise.allSettled([
|
||||
computersApi.policies.configuration.list(),
|
||||
computersApi.types.list({ perpage: 200 }),
|
||||
applicationsApi.listAll({ perpage: 200 }),
|
||||
computersApi.policies.actions()
|
||||
computersApi.policies.actions(),
|
||||
endpointsApi.monitors.list()
|
||||
])
|
||||
policies.value = pol.data.data || []
|
||||
pctypes.value = types.data.data || []
|
||||
// applicationsApi.listAll resolves to a plain array (fetchAllPages), not an
|
||||
// axios response, so read it directly. Reading .data on the array throws and
|
||||
// rejects the whole Promise.all, blanking every dropdown.
|
||||
apps.value = Array.isArray(appList) ? appList : (appList.data?.data || appList.data || [])
|
||||
actionCatalog.value = actions.data.data || []
|
||||
} catch (err) {
|
||||
console.error('Error loading configuration policies:', err)
|
||||
|
||||
const failures = []
|
||||
|
||||
if (pol.status === 'fulfilled') {
|
||||
policies.value = pol.value.data.data || []
|
||||
} else {
|
||||
failures.push('policies')
|
||||
console.error('Error loading policies:', pol.reason)
|
||||
}
|
||||
|
||||
if (types.status === 'fulfilled') {
|
||||
pctypes.value = types.value.data.data || []
|
||||
} else {
|
||||
failures.push('PC types')
|
||||
console.error('Error loading PC types:', types.reason)
|
||||
}
|
||||
|
||||
if (appList.status === 'fulfilled') {
|
||||
// applicationsApi.listAll resolves to a plain array (fetchAllPages), not
|
||||
// an axios response, so read it directly.
|
||||
const value = appList.value
|
||||
apps.value = Array.isArray(value) ? value : (value.data?.data || value.data || [])
|
||||
} else {
|
||||
failures.push('applications')
|
||||
console.error('Error loading applications:', appList.reason)
|
||||
}
|
||||
|
||||
if (actions.status === 'fulfilled') {
|
||||
actionCatalog.value = actions.value.data.data || []
|
||||
} else {
|
||||
failures.push('actions')
|
||||
console.error('Error loading actions:', actions.reason)
|
||||
}
|
||||
|
||||
if (monitorList.status === 'fulfilled') {
|
||||
monitors.value = monitorList.value.data.data || []
|
||||
} else {
|
||||
failures.push('monitors')
|
||||
console.error('Error loading monitors:', monitorList.reason)
|
||||
}
|
||||
|
||||
if (failures.length) {
|
||||
toast.error(`Failed to load: ${failures.join(', ')}. Some options may be unavailable.`)
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -177,6 +224,30 @@ function addAction() {
|
||||
form.value.actions.push({ action: '', mode: 'gate' })
|
||||
}
|
||||
|
||||
function onMonitorCreated(monitor) {
|
||||
// Add the new definition to the local catalog so its label resolves and it
|
||||
// is searchable on other action rows without a full reload.
|
||||
if (monitor && !monitors.value.some(m => m.endpointmonitorid === monitor.endpointmonitorid)) {
|
||||
monitors.value.push(monitor)
|
||||
}
|
||||
}
|
||||
|
||||
function actionLabel(action) {
|
||||
if (!action) return action
|
||||
if (action.startsWith('monitor:')) {
|
||||
const id = Number(action.slice('monitor:'.length))
|
||||
const monitor = monitors.value.find(m => m.endpointmonitorid === id)
|
||||
return monitor ? `Monitor: ${monitor.displayname}` : action
|
||||
}
|
||||
const command = actionCatalog.value.find(c => c.action === action)
|
||||
return command ? (command.label || command.action) : action
|
||||
}
|
||||
|
||||
function showDesiredWarning(action) {
|
||||
return !!action && action.mode === 'desired'
|
||||
&& !!action.action && action.action.startsWith('monitor:')
|
||||
}
|
||||
|
||||
async function save() {
|
||||
saving.value = true
|
||||
error.value = ''
|
||||
@@ -213,11 +284,35 @@ async function remove(p) {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.modal-lg {
|
||||
max-width: 760px;
|
||||
}
|
||||
.modal-lg .modal-body {
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.modal-lg :deep(.form-group) {
|
||||
margin-bottom: 1.1rem;
|
||||
}
|
||||
.modal-lg :deep(.form-control) {
|
||||
font-size: 14px;
|
||||
padding: 0.55rem 0.7rem;
|
||||
}
|
||||
.scope-builder {
|
||||
padding: 10px;
|
||||
border: 1px solid var(--border-color, #444);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.action-row { display: flex; gap: 8px; align-items: center; margin-bottom: 6px; }
|
||||
.action-row { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-bottom: 6px; }
|
||||
.mode-select { max-width: 160px; }
|
||||
.desired-warning {
|
||||
flex-basis: 100%;
|
||||
margin: 2px 0 0;
|
||||
padding: 6px 8px;
|
||||
font-size: 0.85em;
|
||||
color: var(--warning-text, #8a6d3b);
|
||||
background: var(--warning-bg, rgba(240, 173, 78, 0.12));
|
||||
border: 1px solid var(--warning-border, rgba(240, 173, 78, 0.4));
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user