automation fixes
Some checks failed
Deploy Plattform-Core (Int) / test (push) Failing after 1m9s
Deploy Plattform-Core (Int) / deploy (push) Has been skipped

This commit is contained in:
ValueOn AG 2026-06-09 22:59:26 +02:00
parent ebc4b2a080
commit 06e68c343b
17 changed files with 207 additions and 85 deletions

2
app.py
View file

@ -501,6 +501,8 @@ async def lifespan(app: FastAPI):
return return
if isinstance(exc, ConnectionAbortedError): if isinstance(exc, ConnectionAbortedError):
return return
if exc and "LocalProtocolError" in type(exc).__name__:
return
loop.default_exception_handler(ctx) loop.default_exception_handler(ctx)
main_loop.set_exception_handler(_suppressClientDisconnect) main_loop.set_exception_handler(_suppressClientDisconnect)
except RuntimeError: except RuntimeError:

View file

@ -150,52 +150,21 @@ NAVIGATION_SECTIONS = [
}, },
], ],
}, },
# --- Workflow-Automation (System-Komponente, cross-mandate) --- # --- Solution Design (System-Komponente, cross-mandate) ---
# Single nav entry; tabs are managed internally by WorkflowAutomationHubPage.
{ {
"id": "workflowAutomation", "id": "workflowAutomation",
"title": t("Workflow-Automation"), "title": t("Lösungsdesign"),
"order": 25, "order": 25,
"items": [ "items": [
{ {
"id": "wa-workflows", "id": "wa-hub",
"objectKey": "ui.system.workflowAutomation.workflows", "objectKey": "ui.system.workflowAutomation",
"label": t("Workflows"), "label": t("Workflow-Automation"),
"icon": "FaSitemap", "icon": "FaSitemap",
"path": "/workflow-automation?tab=workflows", "path": "/workflow-automation",
"order": 10, "order": 10,
}, },
{
"id": "wa-editor",
"objectKey": "ui.system.workflowAutomation.editor",
"label": t("Editor"),
"icon": "FaProjectDiagram",
"path": "/workflow-automation?tab=editor",
"order": 20,
},
{
"id": "wa-templates",
"objectKey": "ui.system.workflowAutomation.templates",
"label": t("Vorlagen"),
"icon": "FaCopy",
"path": "/workflow-automation?tab=templates",
"order": 30,
},
{
"id": "wa-runs",
"objectKey": "ui.system.workflowAutomation.runs",
"label": t("Läufe"),
"icon": "FaPlay",
"path": "/workflow-automation?tab=runs",
"order": 40,
},
{
"id": "wa-tasks",
"objectKey": "ui.system.workflowAutomation.tasks",
"label": t("Tasks"),
"icon": "FaTasks",
"path": "/workflow-automation?tab=tasks",
"order": 50,
},
], ],
}, },
# --- Administration (with subgroups) --- # --- Administration (with subgroups) ---
@ -237,7 +206,7 @@ NAVIGATION_SECTIONS = [
{ {
"id": "admin-users", "id": "admin-users",
"objectKey": "ui.admin.users", "objectKey": "ui.admin.users",
"label": t("Benutzer"), "label": t("Übersicht"),
"icon": "FaUsers", "icon": "FaUsers",
"path": "/admin/users", "path": "/admin/users",
"order": 10, "order": 10,
@ -246,7 +215,7 @@ NAVIGATION_SECTIONS = [
{ {
"id": "admin-invitations", "id": "admin-invitations",
"objectKey": "ui.admin.invitations", "objectKey": "ui.admin.invitations",
"label": t("Benutzer-Einladungen"), "label": t("Einladungen"),
"icon": "FaEnvelopeOpenText", "icon": "FaEnvelopeOpenText",
"path": "/admin/invitations", "path": "/admin/invitations",
"order": 20, "order": 20,
@ -255,7 +224,7 @@ NAVIGATION_SECTIONS = [
{ {
"id": "admin-user-access-overview", "id": "admin-user-access-overview",
"objectKey": "ui.admin.userAccessOverview", "objectKey": "ui.admin.userAccessOverview",
"label": t("Benutzer-Zugriffsübersicht"), "label": t("Zugriffe"),
"icon": "FaClipboardList", "icon": "FaClipboardList",
"path": "/admin/user-access-overview", "path": "/admin/user-access-overview",
"order": 30, "order": 30,

View file

@ -96,6 +96,23 @@ def resolveRoleLabels(db, ids: List[str]) -> Dict[str, Optional[str]]:
return out return out
def resolveFileLabels(db, ids: List[str]) -> Dict[str, Optional[str]]:
"""Resolve FileItem IDs to fileName. Returns None for unresolvable."""
if not ids:
return {}
from modules.datamodels.datamodelFiles import FileItem as _FileItem
recs = db.getRecordset(
_FileItem,
recordFilter={"id": list(set(ids))},
) or []
out: Dict[str, Optional[str]] = {i: None for i in ids}
for r in recs:
fid = r.get("id")
if fid:
out[fid] = r.get("fileName") or None
return out
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Resolver registry # Resolver registry
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -105,6 +122,7 @@ _BUILTIN_FK_RESOLVERS: Dict[str, Callable] = {
"FeatureInstance": resolveInstanceLabels, "FeatureInstance": resolveInstanceLabels,
"UserInDB": resolveUserLabels, "UserInDB": resolveUserLabels,
"Role": resolveRoleLabels, "Role": resolveRoleLabels,
"FileItem": resolveFileLabels,
} }

View file

@ -263,7 +263,7 @@ def get_projects(
if mode == "filterValues": if mode == "filterValues":
if not column: if not column:
raise HTTPException(status_code=400, detail="column parameter required for mode=filterValues") raise HTTPException(status_code=400, detail="column parameter required for mode=filterValues")
enrichRowsWithFkLabels(itemDicts, Projekt, db=interface.db) enrichRowsWithFkLabels(itemDicts, Projekt, db=getRootInterface().db)
return handleFilterValuesInMemory(itemDicts, column, pagination) return handleFilterValuesInMemory(itemDicts, column, pagination)
return handleIdsInMemory(itemDicts, pagination) return handleIdsInMemory(itemDicts, pagination)
@ -271,7 +271,9 @@ def get_projects(
paginationParams = _parsePagination(pagination) paginationParams = _parsePagination(pagination)
if paginationParams: if paginationParams:
from modules.dbHelpers.paginationHelpers import applyFiltersAndSort from modules.dbHelpers.paginationHelpers import applyFiltersAndSort
from modules.dbHelpers.fkLabelResolver import enrichRowsWithFkLabels
itemDicts = [i.model_dump() if hasattr(i, 'model_dump') else i for i in items] itemDicts = [i.model_dump() if hasattr(i, 'model_dump') else i for i in items]
enrichRowsWithFkLabels(itemDicts, Projekt, db=getRootInterface().db)
filtered = applyFiltersAndSort(itemDicts, paginationParams) filtered = applyFiltersAndSort(itemDicts, paginationParams)
total_items = len(filtered) total_items = len(filtered)
total_pages = (total_items + paginationParams.pageSize - 1) // paginationParams.pageSize total_pages = (total_items + paginationParams.pageSize - 1) // paginationParams.pageSize
@ -289,7 +291,10 @@ def get_projects(
filters=paginationParams.filters filters=paginationParams.filters
) )
) )
return PaginatedResponse(items=items, pagination=None) from modules.dbHelpers.fkLabelResolver import enrichRowsWithFkLabels
itemDicts = [i.model_dump() if hasattr(i, 'model_dump') else i for i in items]
enrichRowsWithFkLabels(itemDicts, Projekt, db=getRootInterface().db)
return PaginatedResponse(items=itemDicts, pagination=None)
@router.get("/{instanceId}/projects/{projectId}", response_model=Projekt) @router.get("/{instanceId}/projects/{projectId}", response_model=Projekt)
@ -405,7 +410,7 @@ def get_parcels(
if mode == "filterValues": if mode == "filterValues":
if not column: if not column:
raise HTTPException(status_code=400, detail="column parameter required for mode=filterValues") raise HTTPException(status_code=400, detail="column parameter required for mode=filterValues")
enrichRowsWithFkLabels(itemDicts, Parzelle, db=interface.db) enrichRowsWithFkLabels(itemDicts, Parzelle, db=getRootInterface().db)
return handleFilterValuesInMemory(itemDicts, column, pagination) return handleFilterValuesInMemory(itemDicts, column, pagination)
return handleIdsInMemory(itemDicts, pagination) return handleIdsInMemory(itemDicts, pagination)
@ -413,7 +418,9 @@ def get_parcels(
paginationParams = _parsePagination(pagination) paginationParams = _parsePagination(pagination)
if paginationParams: if paginationParams:
from modules.dbHelpers.paginationHelpers import applyFiltersAndSort from modules.dbHelpers.paginationHelpers import applyFiltersAndSort
from modules.dbHelpers.fkLabelResolver import enrichRowsWithFkLabels
itemDicts = [i.model_dump() if hasattr(i, 'model_dump') else i for i in items] itemDicts = [i.model_dump() if hasattr(i, 'model_dump') else i for i in items]
enrichRowsWithFkLabels(itemDicts, Parzelle, db=getRootInterface().db)
filtered = applyFiltersAndSort(itemDicts, paginationParams) filtered = applyFiltersAndSort(itemDicts, paginationParams)
total_items = len(filtered) total_items = len(filtered)
total_pages = (total_items + paginationParams.pageSize - 1) // paginationParams.pageSize total_pages = (total_items + paginationParams.pageSize - 1) // paginationParams.pageSize
@ -431,7 +438,10 @@ def get_parcels(
filters=paginationParams.filters filters=paginationParams.filters
) )
) )
return PaginatedResponse(items=items, pagination=None) from modules.dbHelpers.fkLabelResolver import enrichRowsWithFkLabels
itemDicts = [i.model_dump() if hasattr(i, 'model_dump') else i for i in items]
enrichRowsWithFkLabels(itemDicts, Parzelle, db=getRootInterface().db)
return PaginatedResponse(items=itemDicts, pagination=None)
@router.get("/{instanceId}/parcels/{parcelId}", response_model=Parzelle) @router.get("/{instanceId}/parcels/{parcelId}", response_model=Parzelle)

View file

@ -437,7 +437,7 @@ def get_organisations(
return [r.model_dump() if hasattr(r, "model_dump") else r for r in items] return [r.model_dump() if hasattr(r, "model_dump") else r for r in items]
if paginationParams and hasattr(result, 'items'): if paginationParams and hasattr(result, 'items'):
enriched = enrichRowsWithFkLabels(_toDicts(result.items), TrusteeOrganisation, db=interface.db) enriched = enrichRowsWithFkLabels(_toDicts(result.items), TrusteeOrganisation, db=getRootInterface().db)
return { return {
"items": enriched, "items": enriched,
"pagination": PaginationMetadata( "pagination": PaginationMetadata(
@ -450,7 +450,7 @@ def get_organisations(
).model_dump(), ).model_dump(),
} }
items = result if isinstance(result, list) else result.items items = result if isinstance(result, list) else result.items
enriched = enrichRowsWithFkLabels(_toDicts(items), TrusteeOrganisation, db=interface.db) enriched = enrichRowsWithFkLabels(_toDicts(items), TrusteeOrganisation, db=getRootInterface().db)
return {"items": enriched, "pagination": None} return {"items": enriched, "pagination": None}
@ -557,7 +557,7 @@ def get_roles(
return [r.model_dump() if hasattr(r, "model_dump") else r for r in items] return [r.model_dump() if hasattr(r, "model_dump") else r for r in items]
if paginationParams and hasattr(result, 'items'): if paginationParams and hasattr(result, 'items'):
enriched = enrichRowsWithFkLabels(_toDicts(result.items), TrusteeRole, db=interface.db) enriched = enrichRowsWithFkLabels(_toDicts(result.items), TrusteeRole, db=getRootInterface().db)
return { return {
"items": enriched, "items": enriched,
"pagination": PaginationMetadata( "pagination": PaginationMetadata(
@ -570,7 +570,7 @@ def get_roles(
).model_dump(), ).model_dump(),
} }
items = result if isinstance(result, list) else result.items items = result if isinstance(result, list) else result.items
enriched = enrichRowsWithFkLabels(_toDicts(items), TrusteeRole, db=interface.db) enriched = enrichRowsWithFkLabels(_toDicts(items), TrusteeRole, db=getRootInterface().db)
return {"items": enriched, "pagination": None} return {"items": enriched, "pagination": None}
@ -677,7 +677,7 @@ def get_all_access(
return [r.model_dump() if hasattr(r, "model_dump") else r for r in items] return [r.model_dump() if hasattr(r, "model_dump") else r for r in items]
if paginationParams and hasattr(result, 'items'): if paginationParams and hasattr(result, 'items'):
enriched = enrichRowsWithFkLabels(_toDicts(result.items), TrusteeAccess, db=interface.db) enriched = enrichRowsWithFkLabels(_toDicts(result.items), TrusteeAccess, db=getRootInterface().db)
return { return {
"items": enriched, "items": enriched,
"pagination": PaginationMetadata( "pagination": PaginationMetadata(
@ -690,7 +690,7 @@ def get_all_access(
).model_dump(), ).model_dump(),
} }
items = result if isinstance(result, list) else result.items items = result if isinstance(result, list) else result.items
enriched = enrichRowsWithFkLabels(_toDicts(items), TrusteeAccess, db=interface.db) enriched = enrichRowsWithFkLabels(_toDicts(items), TrusteeAccess, db=getRootInterface().db)
return {"items": enriched, "pagination": None} return {"items": enriched, "pagination": None}
@ -827,7 +827,7 @@ def get_contracts(
return [r.model_dump() if hasattr(r, "model_dump") else r for r in items] return [r.model_dump() if hasattr(r, "model_dump") else r for r in items]
if paginationParams and hasattr(result, 'items'): if paginationParams and hasattr(result, 'items'):
enriched = enrichRowsWithFkLabels(_toDicts(result.items), TrusteeContract, db=interface.db) enriched = enrichRowsWithFkLabels(_toDicts(result.items), TrusteeContract, db=getRootInterface().db)
return { return {
"items": enriched, "items": enriched,
"pagination": PaginationMetadata( "pagination": PaginationMetadata(
@ -840,7 +840,7 @@ def get_contracts(
).model_dump(), ).model_dump(),
} }
items = result if isinstance(result, list) else result.items items = result if isinstance(result, list) else result.items
enriched = enrichRowsWithFkLabels(_toDicts(items), TrusteeContract, db=interface.db) enriched = enrichRowsWithFkLabels(_toDicts(items), TrusteeContract, db=getRootInterface().db)
return {"items": enriched, "pagination": None} return {"items": enriched, "pagination": None}
@ -953,6 +953,7 @@ def get_documents(
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
): ):
"""Get all documents (metadata only) with optional pagination.""" """Get all documents (metadata only) with optional pagination."""
from modules.dbHelpers.fkLabelResolver import enrichRowsWithFkLabels
mandateId = _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
if mode in ("filterValues", "ids"): if mode in ("filterValues", "ids"):
@ -966,8 +967,9 @@ def get_documents(
return [r.model_dump() if hasattr(r, 'model_dump') else r for r in items] return [r.model_dump() if hasattr(r, 'model_dump') else r for r in items]
if paginationParams and hasattr(result, 'items'): if paginationParams and hasattr(result, 'items'):
enriched = enrichRowsWithFkLabels(_itemsToDicts(result.items), TrusteeDocument, db=getRootInterface().db)
return { return {
"items": _itemsToDicts(result.items), "items": enriched,
"pagination": PaginationMetadata( "pagination": PaginationMetadata(
currentPage=paginationParams.page or 1, currentPage=paginationParams.page or 1,
pageSize=paginationParams.pageSize or 20, pageSize=paginationParams.pageSize or 20,
@ -978,7 +980,8 @@ def get_documents(
).model_dump(), ).model_dump(),
} }
items = result if isinstance(result, list) else result.items items = result if isinstance(result, list) else result.items
return {"items": _itemsToDicts(items), "pagination": None} enriched = enrichRowsWithFkLabels(_itemsToDicts(items), TrusteeDocument, db=getRootInterface().db)
return {"items": enriched, "pagination": None}
def _handleDocumentMode(instanceId, mandateId, mode, column, pagination, context): def _handleDocumentMode(instanceId, mandateId, mode, column, pagination, context):
@ -991,7 +994,7 @@ def _handleDocumentMode(instanceId, mandateId, mode, column, pagination, context
raise HTTPException(status_code=400, detail="column parameter required for mode=filterValues") raise HTTPException(status_code=400, detail="column parameter required for mode=filterValues")
result = interface.getAllDocuments(None) result = interface.getAllDocuments(None)
items = [r.model_dump() if hasattr(r, 'model_dump') else r for r in (result.items if hasattr(result, 'items') else result)] items = [r.model_dump() if hasattr(r, 'model_dump') else r for r in (result.items if hasattr(result, 'items') else result)]
enrichRowsWithFkLabels(items, TrusteeDocument, db=interface.db) enrichRowsWithFkLabels(items, TrusteeDocument, db=getRootInterface().db)
return handleFilterValuesInMemory(items, column, pagination) return handleFilterValuesInMemory(items, column, pagination)
if mode == "ids": if mode == "ids":
result = interface.getAllDocuments(None) result = interface.getAllDocuments(None)
@ -1229,6 +1232,7 @@ def get_positions(
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
): ):
"""Get all positions with optional pagination.""" """Get all positions with optional pagination."""
from modules.dbHelpers.fkLabelResolver import enrichRowsWithFkLabels
mandateId = _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
if mode in ("filterValues", "ids"): if mode in ("filterValues", "ids"):
@ -1241,9 +1245,12 @@ def get_positions(
def _itemsToDicts(items): def _itemsToDicts(items):
return [r.model_dump() if hasattr(r, 'model_dump') else r for r in items] return [r.model_dump() if hasattr(r, 'model_dump') else r for r in items]
featureResolvers = _buildFeatureInternalResolvers(TrusteePosition, interface.db)
if paginationParams and hasattr(result, 'items'): if paginationParams and hasattr(result, 'items'):
items = _itemsToDicts(result.items) items = _itemsToDicts(result.items)
_enrichPositionsWithSyncStatus(items, interface, instanceId) _enrichPositionsWithSyncStatus(items, interface, instanceId)
enrichRowsWithFkLabels(items, TrusteePosition, db=getRootInterface().db, extraResolvers=featureResolvers or None)
return { return {
"items": items, "items": items,
"pagination": PaginationMetadata( "pagination": PaginationMetadata(
@ -1258,6 +1265,7 @@ def get_positions(
rawItems = result if isinstance(result, list) else result.items rawItems = result if isinstance(result, list) else result.items
items = _itemsToDicts(rawItems) items = _itemsToDicts(rawItems)
_enrichPositionsWithSyncStatus(items, interface, instanceId) _enrichPositionsWithSyncStatus(items, interface, instanceId)
enrichRowsWithFkLabels(items, TrusteePosition, db=getRootInterface().db, extraResolvers=featureResolvers or None)
return {"items": items, "pagination": None} return {"items": items, "pagination": None}
@ -1273,7 +1281,7 @@ def _handlePositionMode(instanceId, mandateId, mode, column, pagination, context
result = interface.getAllPositions(None) result = interface.getAllPositions(None)
items = [r.model_dump() if hasattr(r, 'model_dump') else r for r in (result.items if hasattr(result, 'items') else result)] items = [r.model_dump() if hasattr(r, 'model_dump') else r for r in (result.items if hasattr(result, 'items') else result)]
_enrichPositionsWithSyncStatus(items, interface, instanceId) _enrichPositionsWithSyncStatus(items, interface, instanceId)
enrichRowsWithFkLabels(items, TrusteePositionView, db=interface.db) enrichRowsWithFkLabels(items, TrusteePositionView, db=getRootInterface().db)
return handleFilterValuesInMemory(items, column, pagination) return handleFilterValuesInMemory(items, column, pagination)
if mode == "ids": if mode == "ids":
result = interface.getAllPositions(None) result = interface.getAllPositions(None)
@ -2075,7 +2083,7 @@ def _paginatedReadEndpoint(
rawItems = result.items if hasattr(result, "items") else result rawItems = result.items if hasattr(result, "items") else result
items = [r.model_dump() if hasattr(r, "model_dump") else r for r in rawItems] items = [r.model_dump() if hasattr(r, "model_dump") else r for r in rawItems]
featureResolvers = _buildFeatureInternalResolvers(modelClass, interface.db) featureResolvers = _buildFeatureInternalResolvers(modelClass, interface.db)
enrichRowsWithFkLabels(items, modelClass, db=interface.db, extraResolvers=featureResolvers or None) enrichRowsWithFkLabels(items, modelClass, db=getRootInterface().db, extraResolvers=featureResolvers or None)
return handleFilterValuesInMemory(items, column, pagination) return handleFilterValuesInMemory(items, column, pagination)
if mode == "ids": if mode == "ids":
@ -2113,7 +2121,7 @@ def _paginatedReadEndpoint(
if paginationParams and hasattr(result, "items"): if paginationParams and hasattr(result, "items"):
enriched = enrichRowsWithFkLabels( enriched = enrichRowsWithFkLabels(
_itemsToDicts(result.items), modelClass, _itemsToDicts(result.items), modelClass,
db=interface.db, extraResolvers=featureResolvers or None, db=getRootInterface().db, extraResolvers=featureResolvers or None,
) )
return { return {
"items": enriched, "items": enriched,
@ -2129,7 +2137,7 @@ def _paginatedReadEndpoint(
items = result.items if hasattr(result, "items") else result items = result.items if hasattr(result, "items") else result
enriched = enrichRowsWithFkLabels( enriched = enrichRowsWithFkLabels(
_itemsToDicts(items), modelClass, _itemsToDicts(items), modelClass,
db=interface.db, extraResolvers=featureResolvers or None, db=getRootInterface().db, extraResolvers=featureResolvers or None,
) )
return {"items": enriched, "pagination": None} return {"items": enriched, "pagination": None}

View file

@ -472,12 +472,13 @@ def list_feature_instances(
items = [inst.model_dump() for inst in instances] items = [inst.model_dump() for inst in instances]
from modules.dbHelpers.fkLabelResolver import enrichRowsWithFkLabels
from modules.datamodels.datamodelFeatures import FeatureInstance
enrichRowsWithFkLabels(items, FeatureInstance, db=rootInterface.db)
if mode == "filterValues": if mode == "filterValues":
if not column: if not column:
raise HTTPException(status_code=400, detail="column parameter required for mode=filterValues") raise HTTPException(status_code=400, detail="column parameter required for mode=filterValues")
from modules.dbHelpers.fkLabelResolver import enrichRowsWithFkLabels
from modules.datamodels.datamodelFeatures import FeatureInstance
enrichRowsWithFkLabels(items, FeatureInstance, db=rootInterface.db)
return handleFilterValuesInMemory(items, column, pagination) return handleFilterValuesInMemory(items, column, pagination)
if mode == "ids": if mode == "ids":

View file

@ -940,6 +940,8 @@ def list_roles(
if paginationParams: if paginationParams:
from modules.dbHelpers.paginationHelpers import applyFiltersAndSort from modules.dbHelpers.paginationHelpers import applyFiltersAndSort
from modules.dbHelpers.fkLabelResolver import enrichRowsWithFkLabels
enrichRowsWithFkLabels(result, Role, db=interface.db)
sortedResult = applyFiltersAndSort(result, paginationParams) sortedResult = applyFiltersAndSort(result, paginationParams)
totalItems = len(sortedResult) totalItems = len(sortedResult)
totalPages = math.ceil(totalItems / paginationParams.pageSize) if totalItems > 0 else 0 totalPages = math.ceil(totalItems / paginationParams.pageSize) if totalItems > 0 else 0
@ -959,7 +961,8 @@ def list_roles(
) )
) )
else: else:
# No pagination - return all roles from modules.dbHelpers.fkLabelResolver import enrichRowsWithFkLabels
enrichRowsWithFkLabels(result, Role, db=interface.db)
return PaginatedResponse( return PaginatedResponse(
items=result, items=result,
pagination=None pagination=None

View file

@ -363,6 +363,16 @@ async def getNeutralizationMappings(
_enrichUserAndInstanceLabels(items, context) _enrichUserAndInstanceLabels(items, context)
fileIds = list({r.get("fileId") for r in items if r.get("fileId")})
if fileIds:
from modules.dbHelpers.fkLabelResolver import resolveFileLabels
from modules.interfaces.interfaceDbApp import getRootInterface
fileMap = resolveFileLabels(getRootInterface().db, fileIds)
for r in items:
fid = r.get("fileId")
if fid and fid in fileMap:
r["fileIdLabel"] = fileMap[fid] or fid
if mode == "filterValues" and column: if mode == "filterValues" and column:
items = _applySortFilterSearch(items, filtersJson=filters) items = _applySortFilterSearch(items, filtersJson=filters)
return _distinctColumnValues(items, column) return _distinctColumnValues(items, column)

View file

@ -120,7 +120,7 @@ def get_prompts(
def _promptsToEnrichedDicts(promptItems): def _promptsToEnrichedDicts(promptItems):
dicts = [r.model_dump() if hasattr(r, 'model_dump') else (dict(r) if not isinstance(r, dict) else r) for r in promptItems] dicts = [r.model_dump() if hasattr(r, 'model_dump') else (dict(r) if not isinstance(r, dict) else r) for r in promptItems]
enrichRowsWithFkLabels(dicts, Prompt, db=managementInterface.db) enrichRowsWithFkLabels(dicts, Prompt, db=getAppInterface(currentUser).db)
return dicts return dicts
managementInterface = interfaceDbManagement.getInterface(currentUser) managementInterface = interfaceDbManagement.getInterface(currentUser)

View file

@ -514,6 +514,10 @@ def getAllSubscriptions(
raise HTTPException(status_code=400, detail=f"Invalid pagination parameter: {str(e)}") raise HTTPException(status_code=400, detail=f"Invalid pagination parameter: {str(e)}")
enriched = _buildEnrichedSubscriptions() enriched = _buildEnrichedSubscriptions()
from modules.dbHelpers.fkLabelResolver import enrichRowsWithFkLabels
from modules.datamodels.datamodelSubscription import MandateSubscription
from modules.interfaces.interfaceDbApp import getRootInterface as _getRootIf
enrichRowsWithFkLabels(enriched, MandateSubscription, db=_getRootIf().db)
filtered = applyFiltersAndSort(enriched, paginationParams) filtered = applyFiltersAndSort(enriched, paginationParams)
if paginationParams: if paginationParams:

View file

@ -63,6 +63,8 @@ async def _listWorkflows(
pagination: Optional[str] = Query(default=None), pagination: Optional[str] = Query(default=None),
mandateId: Optional[str] = Query(default=None), mandateId: Optional[str] = Query(default=None),
): ):
from modules.dbHelpers.fkLabelResolver import enrichRowsWithFkLabels
from modules.interfaces.interfaceDbApp import getRootInterface as _getRootIface
db = _getWorkflowAutomationDb() db = _getWorkflowAutomationDb()
try: try:
db._ensureTableExists(AutoWorkflow) db._ensureTableExists(AutoWorkflow)
@ -76,6 +78,7 @@ async def _listWorkflows(
params = _parsePaginationOr400(pagination) params = _parsePaginationOr400(pagination)
records = db.getRecordset(AutoWorkflow, recordFilter=scopeFilter) records = db.getRecordset(AutoWorkflow, recordFilter=scopeFilter)
enrichRowsWithFkLabels(records or [], AutoWorkflow, db=_getRootIface().db)
if params: if params:
filtered = applyFiltersAndSort(records or [], params) filtered = applyFiltersAndSort(records or [], params)
pageItems, totalItems = paginateInMemory(filtered, params) pageItems, totalItems = paginateInMemory(filtered, params)
@ -169,6 +172,8 @@ async def _listRuns(
mandateId: Optional[str] = Query(default=None), mandateId: Optional[str] = Query(default=None),
workflowId: Optional[str] = Query(default=None), workflowId: Optional[str] = Query(default=None),
): ):
from modules.dbHelpers.fkLabelResolver import enrichRowsWithFkLabels
from modules.interfaces.interfaceDbApp import getRootInterface as _getRootIface
db = _getWorkflowAutomationDb() db = _getWorkflowAutomationDb()
try: try:
db._ensureTableExists(AutoRun) db._ensureTableExists(AutoRun)
@ -185,6 +190,15 @@ async def _listRuns(
params = _parsePaginationOr400(pagination) params = _parsePaginationOr400(pagination)
records = db.getRecordset(AutoRun, recordFilter=scopeFilter) records = db.getRecordset(AutoRun, recordFilter=scopeFilter)
def _resolveWorkflowLabels(ids):
wfRecs = db.getRecordset(AutoWorkflow, recordFilter={"id": list(set(ids))}) or []
return {r.get("id"): r.get("label") or r.get("name") for r in wfRecs}
enrichRowsWithFkLabels(
records or [], AutoRun, db=_getRootIface().db,
extraResolvers={"workflowId": _resolveWorkflowLabels},
)
if params: if params:
filtered = applyFiltersAndSort(records or [], params) filtered = applyFiltersAndSort(records or [], params)
pageItems, totalItems = paginateInMemory(filtered, params) pageItems, totalItems = paginateInMemory(filtered, params)
@ -991,7 +1005,7 @@ def _getMetrics(
try: try:
workflows = db.getRecordset(AutoWorkflow, recordFilter=scopeFilter) or [] if db._ensureTableExists(AutoWorkflow) else [] workflows = db.getRecordset(AutoWorkflow, recordFilter=scopeFilter) or [] if db._ensureTableExists(AutoWorkflow) else []
wfIds = [w.get("id") for w in workflows] wfIds = [w.get("id") for w in workflows]
runFilter = {"workflowId": {"$in": wfIds}} if wfIds else {"workflowId": "__none__"} runFilter = {"workflowId": wfIds} if wfIds else {"workflowId": "__none__"}
runs = db.getRecordset(AutoRun, recordFilter=runFilter) or [] if db._ensureTableExists(AutoRun) else [] runs = db.getRecordset(AutoRun, recordFilter=runFilter) or [] if db._ensureTableExists(AutoRun) else []
tasks = db.getRecordset(AutoTask, recordFilter=runFilter) or [] if db._ensureTableExists(AutoTask) else [] tasks = db.getRecordset(AutoTask, recordFilter=runFilter) or [] if db._ensureTableExists(AutoTask) else []
finally: finally:
@ -1275,7 +1289,8 @@ def _getRunDetail(
if tid: if tid:
try: try:
from modules.dbHelpers.fkLabelResolver import resolveInstanceLabels from modules.dbHelpers.fkLabelResolver import resolveInstanceLabels
labelMap = resolveInstanceLabels(db, [tid]) from modules.interfaces.interfaceDbApp import getRootInterface as _getRootIface
labelMap = resolveInstanceLabels(_getRootIface().db, [tid])
targetInstanceLabel = labelMap.get(tid) targetInstanceLabel = labelMap.get(tid)
except Exception: except Exception:
pass pass
@ -1465,6 +1480,85 @@ async def _executeWorkflow(
return result return result
@router.post("/execute")
@limiter.limit("30/minute")
async def _executeWorkflowFromBody(
request: Request,
body: dict = Body(..., description="{ workflowId?, graph?, targetInstanceId?, payload?, runEnvelope? }"),
context: RequestContext = Depends(getRequestContext),
) -> dict:
"""Execute a workflow — workflowId from body or ad-hoc graph execution."""
from modules.workflowAutomation.mainWorkflowAutomation import _getWorkflowAutomationServices
from modules.workflowAutomation.engine.executionEngine import executeGraph
from modules.interfaces.interfaceWorkflowAutomation import _getWorkflowAutomationInterface
from modules.workflows.processing.shared.methodDiscovery import discoverMethods
userId = str(context.user.id) if context.user else None
workflowId = body.get("workflowId") or ""
targetInstanceId = body.get("targetInstanceId") or ""
wf = None
if workflowId:
db = _getWorkflowAutomationDb()
try:
db._ensureTableExists(AutoWorkflow)
wf = db.getRecord(AutoWorkflow, workflowId)
finally:
db.close()
if not wf:
raise HTTPException(status_code=404, detail=routeApiMsg("Workflow not found"))
_validateWorkflowAccess(context, wf, "execute")
mandateId = (wf.get("mandateId") if wf else None) or str(context.mandateId or "")
instanceId = (wf.get("featureInstanceId") if wf else None) or targetInstanceId or str(context.featureInstanceId or "")
targetFeatureInstanceId = (wf.get("targetFeatureInstanceId") if wf else None) or targetInstanceId or ""
services = _getWorkflowAutomationServices(
context.user,
mandateId=mandateId,
featureInstanceId=instanceId,
)
discoverMethods(services)
graph = body.get("graph") or body.get("payload") or {}
if wf and not (graph.get("nodes") or []):
graph = wf.get("graph") or {}
logger.info(
"workflowAutomation /execute: workflowId=%s nodes=%d userId=%s",
workflowId, len(graph.get("nodes") or []), userId,
)
workflowForEnvelope = wf
runEnv = _buildExecuteRunEnvelope(
body,
workflowForEnvelope,
userId,
getattr(context.user, "language", None) if context.user else None,
)
wfLabel = (wf.get("label") if wf else None) or ""
iface = _getWorkflowAutomationInterface(context.user, mandateId, instanceId)
result = await executeGraph(
graph=graph,
services=services,
workflowId=workflowId or None,
instanceId=instanceId,
userId=userId,
mandateId=mandateId,
automation2_interface=iface,
run_envelope=runEnv,
label=wfLabel,
targetFeatureInstanceId=targetFeatureInstanceId,
)
logger.info(
"workflowAutomation /execute result: success=%s error=%s paused=%s",
result.get("success"), result.get("error"), result.get("paused"),
)
_startEmailPollerIfNeeded(result)
return result
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Version management # Version management
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------

View file

@ -1003,6 +1003,9 @@ class ChatService:
"""Get workflow by ID by delegating to the chat interface""" """Get workflow by ID by delegating to the chat interface"""
try: try:
logger.debug(f"getWorkflow called with workflowId: {workflowId}") logger.debug(f"getWorkflow called with workflowId: {workflowId}")
if workflowId.startswith("transient-"):
logger.debug(f"getWorkflow: skipping DB lookup for transient workflow {workflowId}")
return None
result = self.interfaceDbChat.getWorkflow(workflowId) result = self.interfaceDbChat.getWorkflow(workflowId)
if result: if result:
logger.debug(f"getWorkflow returned workflow with ID: {result.id}") logger.debug(f"getWorkflow returned workflow with ID: {result.id}")

View file

@ -21,7 +21,7 @@ model (see wiki/b-reference/platform/unified-data-bar.md):
- FdsRecordNode (+children)-- feature-owned FeatureDataSource records - FdsRecordNode (+children)-- feature-owned FeatureDataSource records
- FdsFieldNode -- virtual per-column nodes under fdsTable - FdsFieldNode -- virtual per-column nodes under fdsTable
The classes use `_inheritFlags.py` as a helper module for the actual The classes use `modules/serviceCenter/core/flagResolution.py` as a helper module for the actual
walk/aggregate/cascade arithmetic, so the inheritance semantics live in walk/aggregate/cascade arithmetic, so the inheritance semantics live in
one place. The classes themselves only express "what does this node type one place. The classes themselves only express "what does this node type
DO" -- ownership, RBAC, persistence routing, child enumeration. DO" -- ownership, RBAC, persistence routing, child enumeration.

View file

@ -770,7 +770,7 @@ async def executeGraph(
waFileLogger: Optional[RunFileLogger] = None waFileLogger: Optional[RunFileLogger] = None
nodeOutputs: Dict[str, Any] = dict(initialNodeOutputs or {}) nodeOutputs: Dict[str, Any] = dict(initialNodeOutputs or {})
if not runId and automation2_interface and workflowId and not is_resume: if not runId and automation2_interface and not is_resume:
run_context = { run_context = {
"connectionMap": connectionMap, "connectionMap": connectionMap,
"inputSources": inputSources, "inputSources": inputSources,

View file

@ -599,9 +599,9 @@ class ActionNodeExecutor:
logger.exception("ActionNodeExecutor node %s FAILED: %s", nodeId, e) logger.exception("ActionNodeExecutor node %s FAILED: %s", nodeId, e)
return _normalizeError(e, outputSchema) return _normalizeError(e, outputSchema)
finally: finally:
if chatService: if self.services.chat:
try: try:
chatService.progressLogFinish(nodeOperationId, actionSuccess) self.services.chat.progressLogFinish(nodeOperationId, actionSuccess)
except Exception: except Exception:
pass pass
@ -630,11 +630,11 @@ class ActionNodeExecutor:
rawBytes = coerceDocumentDataToBytes(rawData) rawBytes = coerceDocumentDataToBytes(rawData)
if isinstance(dumped, dict) and rawBytes: if isinstance(dumped, dict) and rawBytes:
try: try:
_mgmt = self.services.interfaceDbComponent _chatSvc = self.services.chat
_docName = dumped.get("documentName") or f"workflow-result-{nodeId}.bin" _docName = dumped.get("documentName") or f"workflow-result-{nodeId}.bin"
_mimeType = dumped.get("mimeType") or "application/octet-stream" _mimeType = dumped.get("mimeType") or "application/octet-stream"
_fileItem = _mgmt.createFile(_docName, _mimeType, rawBytes, folderId=persist_folder_id) _fileItem = _chatSvc.createFile(_docName, _mimeType, rawBytes, folderId=persist_folder_id)
_mgmt.createFileData(_fileItem.id, rawBytes) _chatSvc.createFileData(_fileItem.id, rawBytes)
dumped["fileId"] = _fileItem.id dumped["fileId"] = _fileItem.id
dumped["id"] = _fileItem.id dumped["id"] = _fileItem.id
dumped["fileName"] = _fileItem.fileName dumped["fileName"] = _fileItem.fileName
@ -656,7 +656,7 @@ class ActionNodeExecutor:
"documents": docsList, "documents": docsList,
"count": len(docsList), "count": len(docsList),
} }
_attachConnectionProvenance(list_out, resolvedParams, outputSchema, chatService, self.services) _attachConnectionProvenance(list_out, resolvedParams, outputSchema, self.services.chat, self.services)
return normalizeToSchema(list_out, outputSchema) return normalizeToSchema(list_out, outputSchema)
extractedContext = "" extractedContext = ""
@ -751,7 +751,7 @@ class ActionNodeExecutor:
"mode": data_dict.get("mode", resolvedParams.get("mode", "summarize")), "mode": data_dict.get("mode", resolvedParams.get("mode", "summarize")),
"count": int(data_dict.get("count", 0)), "count": int(data_dict.get("count", 0)),
} }
_attachConnectionProvenance(cr_out, resolvedParams, outputSchema, chatService, self.services) _attachConnectionProvenance(cr_out, resolvedParams, outputSchema, self.services.chat, self.services)
return normalizeToSchema(cr_out, outputSchema) return normalizeToSchema(cr_out, outputSchema)
if nodeDef.get("popDocumentsFromOutput"): if nodeDef.get("popDocumentsFromOutput"):
@ -760,7 +760,7 @@ class ActionNodeExecutor:
if outputSchema in ("AiResult", "ActionResult") and result.success: if outputSchema in ("AiResult", "ActionResult") and result.success:
_attach_unified_presentation_data(out, node_def=nodeDef) _attach_unified_presentation_data(out, node_def=nodeDef)
_attachConnectionProvenance(out, resolvedParams, outputSchema, chatService, self.services) _attachConnectionProvenance(out, resolvedParams, outputSchema, self.services.chat, self.services)
# When the node declares ``surfaceDataAsTopLevel`` (typical for # When the node declares ``surfaceDataAsTopLevel`` (typical for
# dynamic-schema context nodes whose output keys are graph-defined), # dynamic-schema context nodes whose output keys are graph-defined),

View file

@ -1194,8 +1194,8 @@ def _persist_extracted_image_parts(
) )
return content_extracted_serial, artifacts return content_extracted_serial, artifacts
if services and hasattr(services, "interfaceDbComponent"): if services and hasattr(services, "chat"):
mgmt = services.interfaceDbComponent mgmt = services.chat
else: else:
from modules.interfaces.interfaceDbManagement import getInterface as _get_mgmt from modules.interfaces.interfaceDbManagement import getInterface as _get_mgmt
from modules.security.rootAccess import getRootUser from modules.security.rootAccess import getRootUser
@ -1206,7 +1206,7 @@ def _persist_extracted_image_parts(
return content_extracted_serial, artifacts return content_extracted_serial, artifacts
if not mgmt: if not mgmt:
logger.warning("extractContent image persist: no interfaceDbComponent available") logger.warning("extractContent image persist: no chat service available")
return content_extracted_serial, artifacts return content_extracted_serial, artifacts
stem = re.sub(r"[^\w\-]+", "_", name_stem).strip("_") or "extract" stem = re.sub(r"[^\w\-]+", "_", name_stem).strip("_") or "extract"
@ -1310,11 +1310,11 @@ _IMAGE_MAX_DIMENSION = 1200
def _get_mgmt_for_presentation_render(services: Any) -> Optional[Any]: def _get_mgmt_for_presentation_render(services: Any) -> Optional[Any]:
mgmt = getattr(services, "interfaceDbComponent", None) if services else None
if mgmt:
return mgmt
if not services: if not services:
return None return None
chat = getattr(services, "chat", None)
if chat:
return chat
try: try:
import modules.interfaces.interfaceDbManagement as iface import modules.interfaces.interfaceDbManagement as iface
@ -1385,7 +1385,7 @@ def _load_image_bytes_by_file_id(services: Any, file_id: str) -> Optional[bytes]
if not mgmt or not hasattr(mgmt, "getFileData"): if not mgmt or not hasattr(mgmt, "getFileData"):
raise ValueError( raise ValueError(
"no management interface available to load persisted image bytes — " "no management interface available to load persisted image bytes — "
"services.interfaceDbComponent / mandate / instance must be set" "services.chat / mandate / instance must be set"
) )
return mgmt.getFileData(str(file_id)) return mgmt.getFileData(str(file_id))

View file

@ -17,7 +17,7 @@ import unittest
from typing import List from typing import List
from unittest.mock import MagicMock from unittest.mock import MagicMock
from modules.serviceCenter.services.serviceKnowledge import _inheritFlags from modules.serviceCenter.core import flagResolution as _inheritFlags
def _ds(idVal: str, path: str, **flags) -> dict: def _ds(idVal: str, path: str, **flags) -> dict: