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
233 lines
7.5 KiB
Vue
233 lines
7.5 KiB
Vue
<template>
|
|
<div>
|
|
<div class="page-header">
|
|
<h2>Monitor Definitions</h2>
|
|
<button class="btn btn-primary" @click="openModal()">+ Add Monitor</button>
|
|
</div>
|
|
|
|
<p class="setting-description">
|
|
A monitor definition is a reusable process or service the agent can watch,
|
|
with a bespoke display label separate from the raw OS name. Shipped monitors
|
|
are product-owned and read-only. Attach monitors to PC types through site
|
|
policies so a deployed agent fetches the right set for its type.
|
|
</p>
|
|
|
|
<div class="card">
|
|
<div v-if="loading" class="loading">Loading...</div>
|
|
<template v-else>
|
|
<div class="table-container">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Display name</th>
|
|
<th>Kind</th>
|
|
<th>OS identity</th>
|
|
<th>Launch path</th>
|
|
<th>Active</th>
|
|
<th>Source</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="m in monitors" :key="m.endpointmonitorid">
|
|
<td>{{ m.displayname }}</td>
|
|
<td>{{ m.kind }}</td>
|
|
<td class="cell-truncate" :title="osidentity(m)">{{ osidentity(m) || '-' }}</td>
|
|
<td class="cell-truncate" :title="m.launchpath">{{ m.launchpath || '-' }}</td>
|
|
<td>{{ m.isactive ? 'Yes' : 'No' }}</td>
|
|
<td>
|
|
<span class="badge" :class="m.source === 'local' ? 'badge-secondary' : 'badge-warning'">
|
|
{{ m.source }}
|
|
</span>
|
|
</td>
|
|
<td class="actions">
|
|
<button class="btn btn-secondary btn-sm" :disabled="m.source !== 'local'" @click="openModal(m)">Edit</button>
|
|
<button class="btn btn-danger btn-sm" :disabled="m.source !== 'local'" @click="remove(m)">Delete</button>
|
|
</td>
|
|
</tr>
|
|
<tr v-if="monitors.length === 0">
|
|
<td colspan="7" style="text-align:center; color: var(--text-light);">No monitor definitions</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<div v-if="showModal" class="modal-overlay">
|
|
<div class="modal modal-lg">
|
|
<div class="modal-header">
|
|
<h3>{{ editing ? 'Edit Monitor' : 'Add Monitor' }}</h3>
|
|
</div>
|
|
<form @submit.prevent="save">
|
|
<div class="modal-body">
|
|
<div class="form-group">
|
|
<label>Display name *</label>
|
|
<input v-model="form.displayname" type="text" class="form-control" required />
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Kind *</label>
|
|
<select v-model="form.kind" class="form-control">
|
|
<option value="process">process</option>
|
|
<option value="service">service</option>
|
|
</select>
|
|
</div>
|
|
<div v-if="form.kind === 'process'" class="form-group">
|
|
<label>Process name * <span class="hint">(executable the agent matches, e.g. agent.exe)</span></label>
|
|
<input v-model="form.processname" type="text" class="form-control" />
|
|
</div>
|
|
<div v-else class="form-group">
|
|
<label>Service name * <span class="hint">(Windows service name)</span></label>
|
|
<input v-model="form.servicename" type="text" class="form-control" />
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Launch path <span class="hint">(optional; starts the app)</span></label>
|
|
<input v-model="form.launchpath" type="text" class="form-control" />
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Data collector path <span class="hint">(optional)</span></label>
|
|
<input v-model="form.datacollectorpath" type="text" class="form-control" />
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Log collector path <span class="hint">(optional)</span></label>
|
|
<input v-model="form.logcollectorpath" type="text" class="form-control" />
|
|
</div>
|
|
<div class="form-group">
|
|
<label>
|
|
<input v-model="form.isactive" type="checkbox" />
|
|
Active
|
|
</label>
|
|
</div>
|
|
|
|
<div v-if="error" class="error-message">{{ error }}</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" @click="closeModal">Cancel</button>
|
|
<button type="submit" class="btn btn-primary" :disabled="saving">{{ saving ? 'Saving...' : 'Save' }}</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted } from 'vue'
|
|
import { endpointsApi } from '@/api'
|
|
import { useToast } from '@/composables/toast'
|
|
import { apiError } from '@/utils/apiError'
|
|
|
|
const toast = useToast()
|
|
|
|
const monitors = ref([])
|
|
const loading = ref(true)
|
|
|
|
const showModal = ref(false)
|
|
const editing = ref(null)
|
|
const saving = ref(false)
|
|
const error = ref('')
|
|
|
|
const form = ref(emptyForm())
|
|
|
|
function emptyForm() {
|
|
return {
|
|
kind: 'process',
|
|
displayname: '',
|
|
processname: '',
|
|
servicename: '',
|
|
launchpath: '',
|
|
datacollectorpath: '',
|
|
logcollectorpath: '',
|
|
isactive: true
|
|
}
|
|
}
|
|
|
|
function osidentity(m) {
|
|
return m.kind === 'service' ? m.servicename : m.processname
|
|
}
|
|
|
|
onMounted(() => loadData())
|
|
|
|
async function loadData() {
|
|
loading.value = true
|
|
try {
|
|
const response = await endpointsApi.monitors.list()
|
|
monitors.value = response.data.data || []
|
|
} catch (err) {
|
|
console.error('Error loading monitor definitions:', err)
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
function openModal(item = null) {
|
|
editing.value = item
|
|
error.value = ''
|
|
if (item) {
|
|
form.value = {
|
|
kind: item.kind || 'process',
|
|
displayname: item.displayname || '',
|
|
processname: item.processname || '',
|
|
servicename: item.servicename || '',
|
|
launchpath: item.launchpath || '',
|
|
datacollectorpath: item.datacollectorpath || '',
|
|
logcollectorpath: item.logcollectorpath || '',
|
|
isactive: item.isactive !== false
|
|
}
|
|
} else {
|
|
form.value = emptyForm()
|
|
}
|
|
showModal.value = true
|
|
}
|
|
|
|
function closeModal() { showModal.value = false; editing.value = null }
|
|
|
|
async function save() {
|
|
saving.value = true
|
|
error.value = ''
|
|
try {
|
|
const payload = {
|
|
kind: form.value.kind,
|
|
displayname: form.value.displayname,
|
|
processname: form.value.processname,
|
|
servicename: form.value.servicename,
|
|
launchpath: form.value.launchpath,
|
|
datacollectorpath: form.value.datacollectorpath,
|
|
logcollectorpath: form.value.logcollectorpath,
|
|
isactive: form.value.isactive
|
|
}
|
|
if (editing.value) {
|
|
await endpointsApi.monitors.update(editing.value.endpointmonitorid, payload)
|
|
} else {
|
|
await endpointsApi.monitors.create(payload)
|
|
}
|
|
closeModal()
|
|
loadData()
|
|
} catch (err) {
|
|
error.value = apiError(err, 'Failed to save monitor')
|
|
} finally {
|
|
saving.value = false
|
|
}
|
|
}
|
|
|
|
async function remove(m) {
|
|
if (!confirm(`Delete monitor "${m.displayname}"?`)) return
|
|
try {
|
|
await endpointsApi.monitors.remove(m.endpointmonitorid)
|
|
loadData()
|
|
} catch (err) {
|
|
toast.error(apiError(err, 'Failed to delete'))
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.cell-truncate {
|
|
max-width: 240px;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
.hint { color: var(--text-light); font-weight: normal; }
|
|
</style>
|