Split shipped monitor bundle into per-monitor policies

The product shipped one Configuration Policy carrying every shopfloor app
monitor, wrapped in a shipped Site Policy that applied it to every endpoint.
That bundled two decisions a site cannot separate: which monitors exist, and
which endpoints get them. A site that wanted eDNC on one PC type and MTConnect
on another had to delete the shipped policy and rebuild both halves.

Ship one Configuration Policy per monitor with no PC type attribution, and stop
shipping a Site Policy at all. Composition is a site decision; the product now
supplies parts, not an assembled opinion.

Shipped Configuration Policies become editable and deletable. Provenance stays
a source tag rather than a lock, which is safe because seeding is insert-if-
absent and no reconciling seeder exists, so a site edit is never stomped. The
stale comments claiming shipped rows were read-only and reconciled described
behavior the code never had.
This commit is contained in:
benjamin.baker
2026-09-08 15:22:10 -04:00
parent 68ddf87d3b
commit 8f115ca330
8 changed files with 591 additions and 160 deletions

View File

@@ -19,7 +19,7 @@ from shopdb.api import (db, AuditLog, success_response, error_response,
from .routes import computers_bp
from ..models import (ConfigurationPolicy, ConfigurationPolicyAction,
BusinessPolicy, BusinessPolicyItem, POLICY_SOURCE_LOCAL,
POLICY_SOURCE_AUTOAPP,
POLICY_SOURCE_AUTOAPP, POLICY_SOURCE_SHIPPED,
ACTION_MODE_DESIRED, ACTION_MODE_GATE)
from ..policyflag import policy_flag_enabled
from ..services.apppolicy import disown_if_generated
@@ -262,8 +262,12 @@ def update_configuration_policy(policy_id: int):
'Configuration policy not found', http_code=404)
# An autoapp row is generated from an Application's "Required on all PCs"
# checkbox and IS editable: editing it is precisely how an admin takes
# ownership. Genuinely external sources (shipped/managed) stay read-only.
if policy.source not in (POLICY_SOURCE_LOCAL, POLICY_SOURCE_AUTOAPP):
# ownership. A shipped row is editable for the same reason - the product
# ships a starting point, not an opinion about one site's floor - and keeps
# source='shipped' so provenance survives the edit. Genuinely external
# sources (a future managed owner) stay read-only.
if policy.source not in (POLICY_SOURCE_LOCAL, POLICY_SOURCE_AUTOAPP,
POLICY_SOURCE_SHIPPED):
return error_response(ErrorCodes.CONFLICT,
'Policy is managed externally and is read-only',
http_code=409)
@@ -326,9 +330,11 @@ def delete_configuration_policy(policy_id: int):
if not policy:
return error_response(ErrorCodes.NOT_FOUND,
'Configuration policy not found', http_code=404)
# As with update: a generated row is the site's to remove, an externally
# managed one is not.
if policy.source not in (POLICY_SOURCE_LOCAL, POLICY_SOURCE_AUTOAPP):
# As with update: a generated or shipped row is the site's to remove, an
# externally managed one is not. A site that does not run MTConnect should
# be able to delete its monitor policy outright rather than carry it.
if policy.source not in (POLICY_SOURCE_LOCAL, POLICY_SOURCE_AUTOAPP,
POLICY_SOURCE_SHIPPED):
return error_response(ErrorCodes.CONFLICT,
'Policy is managed externally and is read-only',
http_code=409)