Add application-driven policy generation with an acknowledgement gate

Ticking "Required on all PCs" now generates a configuration policy that
installs the application on every non-server endpoint it matches, rather
than only flagging a software-compliance report row.

Because that save is a fleet-wide deployment, it requires explicit
acknowledgement. The gate is enforced in the API, not only in the form:
a confirmation dialog that can be sidestepped by POSTing directly is
decoration, not a safeguard. It fires only on the transition INTO the
required state, since re-saving an already-required application deploys
nothing new.

Generated policies carry a new 'autoapp' source. They stay editable, and
hand-editing one transfers ownership to 'local' so it survives a later
uncheck; 'shipped' remains read-only. Existing required applications are
not migrated retroactively - policies are only created on save.

Also adds the monitorable fields that the shipped-policy breakup will
build on, and repairs a mangled non-ASCII byte in copilot-instructions.md
that was failing the style gate.
This commit is contained in:
benjamin.baker
2026-09-07 13:27:59 -04:00
parent f73df42110
commit aa9dafc5ca
10 changed files with 497 additions and 7 deletions

View File

@@ -74,6 +74,24 @@
Hidden
</label>
</div>
<!-- Only shown when the user is newly ticking the box. An app that
was already required has an existing policy, so re-saving it is
not a new deployment and does not need re-acknowledging. -->
<div v-if="showsPolicyWarning" class="policy-warning">
<strong>This will install software on your fleet.</strong>
<p>
Marking <em>{{ form.appname || 'this application' }}</em> as required
on all PCs creates a configuration policy that installs it on every
non-server endpoint it applies to. Installation happens
automatically, without further prompting, on each machine as it
checks in.
</p>
<label>
<input type="checkbox" v-model="acknowledgePolicy" />
I understand this will install the application on all matched PCs.
</label>
</div>
</div>
<h4 style="margin-top: 1.5rem; margin-bottom: 1rem;">Links & Paths</h4>
@@ -225,6 +243,15 @@ const form = ref({
const supportTeams = ref([])
// The value as saved, so we can tell a NEW deployment from a re-save of an
// application that was already required. Only the former warrants a warning.
const wasRequired = ref(false)
const acknowledgePolicy = ref(false)
const showsPolicyWarning = computed(
() => form.value.isrequired && !wasRequired.value
)
// Files are chosen before the application exists, so they are held here and
// uploaded once there is an id to attach them to (see saveApplication).
const pendingImage = ref(null)
@@ -322,6 +349,7 @@ onMounted(async () => {
applicationnotes: app.applicationnotes || ''
}
imagePreview.value = imageSrcFor(app.image)
wasRequired.value = app.isrequired || false
if ((app.installpath || '').startsWith('/api/applications/package/')) {
uploadedPackage.value = app.installpath.split('/').pop()
}
@@ -336,6 +364,13 @@ onMounted(async () => {
async function saveApplication() {
error.value = ''
if (showsPolicyWarning.value && !acknowledgePolicy.value) {
error.value = 'Please acknowledge that this will install the application '
+ 'on all matched PCs before saving.'
return
}
saving.value = true
try {
@@ -352,7 +387,8 @@ async function saveApplication() {
documentationpath: form.value.documentationpath || null,
installpath: form.value.installpath || null,
image: form.value.image || null,
applicationnotes: form.value.applicationnotes || null
applicationnotes: form.value.applicationnotes || null,
acknowledgepolicy: showsPolicyWarning.value
}
let appid = route.params.id
@@ -449,4 +485,27 @@ async function saveApplication() {
font-size: 0.8rem;
color: var(--text-light, #666);
}
.policy-warning {
margin-top: 1rem;
padding: 0.75rem 1rem;
border: 1px solid var(--warning, #b45309);
border-left-width: 4px;
border-radius: 4px;
background: var(--warning-bg, #fffbeb);
color: var(--text, #333);
}
.policy-warning p {
margin: 0.5rem 0 0.75rem;
font-size: 0.9rem;
}
.policy-warning label {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
font-weight: 600;
}
</style>