Printer assignment end to end, and a container that keeps a site's files

Assign printers to a MACHINE and let whichever PC controls it install them, with
the observed half kept strictly apart from the assigned half: a collector report
can never write an assignment row. Adoption is explicit. Adds a singular
relationship type so defaultprinter can mean exactly one, client scripts that
converge a bay's queues and correct a drifted one in place, and a wave gate so a
fleet-wide driver rollout cannot stampede its own share.

On the container side: the compose stack never persisted the instance directory,
so a rebuild discarded plugins.json and every upload. It is a volume now, the
image ships instance/ owned by the app user so the mountpoint is writable, and
the database moves to MySQL 8.4 LTS, which the Windows runbook has recommended
since 8.0 went end of life. The image also could not build at all: the frontend
stage never copied the plugin staging script it runs.

Adds a topology migration guide covering IIS to Docker and back.
This commit is contained in:
2026-08-20 07:46:35 -04:00
parent 09e9d5f047
commit d267813999
45 changed files with 6475 additions and 56 deletions

View File

@@ -792,6 +792,27 @@ def create_asset_relationship():
http_code=409
)
# A SINGULAR type allows one active row per source, and setting a new one
# REPLACES rather than refusing: "make this the default printer" means
# exactly that, and a card that answered 409 would leave the user to find
# and delete the old row first.
#
# Without this the schema is happy to hold two defaults - the unique
# constraint is (source, target, type), so two different targets are two
# valid rows - and the resolver takes the OLDEST, so the new default
# silently loses.
reltype = db.session.get(RelationshipType, type_id)
replaced = 0
if reltype is not None and getattr(reltype, 'issingular', False):
others = AssetRelationship.query.filter(
AssetRelationship.sourceassetid == source_id,
AssetRelationship.relationshiptypeid == type_id,
AssetRelationship.isactive == True,
AssetRelationship.targetassetid != target_id).all()
for row in others:
row.isactive = False
replaced += 1
# And it cannot relate BOTH WAYS on a directional type. Only one direction
# can be true - a PC drives a machine, never the reverse - but the check
# above is keyed on (source, target, type), so the inverse used to insert

View File

@@ -24,6 +24,11 @@ class RelationshipType(BaseModel):
# True: edge has a source->target meaning (controls, partof, Backup For).
# False: symmetric link (Dualpath, connectedto, USB...) shown on the card
# once per peer with no direction, both stored direction rows collapsed.
# At most one ACTIVE relationship of this type per source asset. A PC has
# one default printer; the (source, target, type) unique constraint cannot
# express that, because two different targets are two different rows.
issingular = db.Column(db.Boolean, nullable=False, default=False)
isdirectional = db.Column(
db.Boolean,
default=True,