harden CSV import reconciliation and preserve saved model selections

Default fuzzy reconcile threshold to 0 and refuse merges when digit runs differ so numbered labels like Bay 311 never bind to Bay 310. Keep edit forms from clearing saved model fields when category filtering hides untyped rows. Includes in-progress computers policy and endpoints command feature work.
This commit is contained in:
benjamin.baker
2026-08-28 09:29:09 -04:00
parent d68c6db9ab
commit 01734e5449
46 changed files with 4241 additions and 52 deletions

View File

@@ -0,0 +1,84 @@
"""endpoint commands queue + cached MTConnect state.
Adds the endpointcommands queue (one WebUI-issued, server-allowlisted action
per row, claimed and finalized by the agent long-poll) and two cached MTConnect
run-state columns on endpointstates so the WebUI toggle can show a real state
between command polls.
Idempotent: create_table_if_missing guards the new table, and the column adds
check the live schema first so a db.create_all() site (which already built the
columns) re-runs clean on strict MySQL 8.
"""
from alembic import op
from shopdb.plugins.alembic_template import (
create_index_if_missing,
create_table_if_missing,
)
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'endpoints0002commands'
down_revision = 'endpoints0001baseline'
branch_labels = None
depends_on = None
def _has_column(table_name, column_name):
from sqlalchemy import inspect
inspector = inspect(op.get_bind())
if not inspector.has_table(table_name):
return False
return any(col['name'] == column_name
for col in inspector.get_columns(table_name))
def upgrade():
if not _has_column('endpointstates', 'mtconnectrunning'):
op.add_column('endpointstates',
sa.Column('mtconnectrunning', sa.Boolean(),
nullable=True))
if not _has_column('endpointstates', 'mtconnectcheckedat'):
op.add_column('endpointstates',
sa.Column('mtconnectcheckedat', sa.DateTime(),
nullable=True))
create_table_if_missing(
'endpointcommands',
sa.Column('endpointcommandid', sa.Integer(), nullable=False),
sa.Column('assetid', sa.Integer(), nullable=False),
sa.Column('action', sa.String(length=64), nullable=False),
sa.Column('status', sa.String(length=20), nullable=False),
sa.Column('requestedby', sa.Integer(), nullable=True),
sa.Column('resultmessage', sa.Text(), nullable=True),
sa.Column('resultjson', sa.Text(length=16777215), nullable=True),
sa.Column('createdat', sa.DateTime(), nullable=False),
sa.Column('claimedat', sa.DateTime(), nullable=True),
sa.Column('completedat', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['assetid'], ['assets.assetid'],
ondelete='CASCADE'),
sa.PrimaryKeyConstraint('endpointcommandid'),
)
create_index_if_missing('ix_endpointcommands_assetid',
'endpointcommands', ['assetid'])
create_index_if_missing('ix_endpointcommands_status',
'endpointcommands', ['status'])
create_index_if_missing('idx_endpointcommands_assetstatus',
'endpointcommands', ['assetid', 'status'])
def downgrade():
op.drop_index('idx_endpointcommands_assetstatus',
table_name='endpointcommands')
op.drop_index('ix_endpointcommands_status',
table_name='endpointcommands')
op.drop_index('ix_endpointcommands_assetid',
table_name='endpointcommands')
op.drop_table('endpointcommands')
if _has_column('endpointstates', 'mtconnectcheckedat'):
op.drop_column('endpointstates', 'mtconnectcheckedat')
if _has_column('endpointstates', 'mtconnectrunning'):
op.drop_column('endpointstates', 'mtconnectrunning')

View File

@@ -0,0 +1,50 @@
"""endpoint command target for generic service/process actions.
Adds nullable target and targetkind columns to endpointcommands so generic
service_* / process_* actions can carry their subject (a Windows service name or
a process image like 'DncMain.exe') over the wire. Fixed-target actions
(mtconnect_*) leave both null. This is the schema half of making the agent
data-driven: a site builds the agent once and every future policy/app is server
data, no agent redistribution.
Idempotent: each add checks the live schema first so a db.create_all() site
(which already built the columns) re-runs clean on strict MySQL 8.
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'endpoints0003cmdtarget'
down_revision = 'endpoints0002commands'
branch_labels = None
depends_on = None
def _has_column(table_name, column_name):
from sqlalchemy import inspect
inspector = inspect(op.get_bind())
if not inspector.has_table(table_name):
return False
return any(col['name'] == column_name
for col in inspector.get_columns(table_name))
def upgrade():
if not _has_column('endpointcommands', 'target'):
op.add_column('endpointcommands',
sa.Column('target', sa.String(length=255),
nullable=True))
if not _has_column('endpointcommands', 'targetkind'):
op.add_column('endpointcommands',
sa.Column('targetkind', sa.String(length=16),
nullable=True))
def downgrade():
if _has_column('endpointcommands', 'targetkind'):
op.drop_column('endpointcommands', 'targetkind')
if _has_column('endpointcommands', 'target'):
op.drop_column('endpointcommands', 'target')