@@ -287,6 +287,18 @@ async function save() {
}
}
+// Mirrors the API guard in plugins/computers/api/policy_routes.py: local,
+// autoapp and shipped rows are all editable. A shipped row is a starting point
+// the product supplies, not a fixed opinion, and it keeps source='shipped' so
+// provenance survives the edit. Only a genuinely external managed owner is
+// read-only. Disabling on source==='local' alone stranded shipped rows behind
+// dead buttons while the API was accepting the write.
+const EDITABLE_SOURCES = ['local', 'autoapp', 'shipped']
+
+function iseditable(p) {
+ return EDITABLE_SOURCES.includes(p.source)
+}
+
async function remove(p) {
if (!confirm(`Delete configuration policy "${p.name}"?`)) return
try {
diff --git a/plugins/computers/frontend/views/configurationPolicyEditable.spec.js b/plugins/computers/frontend/views/configurationPolicyEditable.spec.js
new file mode 100644
index 00000000..9dec5bdc
--- /dev/null
+++ b/plugins/computers/frontend/views/configurationPolicyEditable.spec.js
@@ -0,0 +1,27 @@
+import { describe, it, expect } from 'vitest'
+import { readFileSync } from 'fs'
+import { fileURLToPath } from 'url'
+import { dirname, join } from 'path'
+
+const here = dirname(fileURLToPath(import.meta.url))
+const view = readFileSync(join(here, 'ConfigurationPoliciesList.vue'), 'utf8')
+
+// The API decides what may be edited; the buttons only reflect that decision.
+// These drifted once: policy_routes.py was widened to accept shipped rows while
+// the buttons still disabled on source !== 'local', so every shipped policy sat
+// behind a dead Edit button and the change looked like it had never rolled.
+// If the API guard changes, EDITABLE_SOURCES here changes with it.
+describe('the configuration policy buttons follow the API guard', () => {
+ it('treats shipped as editable, not read-only', () => {
+ expect(view).toContain("EDITABLE_SOURCES = ['local', 'autoapp', 'shipped']")
+ })
+
+ it('does not gate the buttons on local alone', () => {
+ expect(view).not.toContain(":disabled=\"p.source !== 'local'\"")
+ })
+
+ it('gates edit and delete on the same rule', () => {
+ const disabled = view.match(/:disabled="!iseditable\(p\)"/g) || []
+ expect(disabled).toHaveLength(2)
+ })
+})