fixed tables and forms

This commit is contained in:
ValueOn AG 2026-03-22 22:19:50 +01:00
parent 2e7a0a73c7
commit 9ef0d43091
8 changed files with 253 additions and 6 deletions

3
app.py
View file

@ -568,6 +568,9 @@ app.include_router(sharepointRouter)
from modules.routes.routeAdminAutomationEvents import router as adminAutomationEventsRouter
app.include_router(adminAutomationEventsRouter)
from modules.routes.routeAdminAutomationLogs import router as adminAutomationLogsRouter
app.include_router(adminAutomationLogsRouter)
from modules.routes.routeAdminLogs import router as adminLogsRouter
app.include_router(adminLogsRouter)

View file

@ -992,6 +992,10 @@ class DatabaseConnector:
Returns (where_clause, order_clause, limit_clause, values, count_values).
"""
fields = _get_model_fields(model_class)
fields["_createdAt"] = "DOUBLE PRECISION"
fields["_modifiedAt"] = "DOUBLE PRECISION"
fields["_createdBy"] = "TEXT"
fields["_modifiedBy"] = "TEXT"
validColumns = set(fields.keys())
where_parts: List[str] = []
values: List[Any] = []
@ -1186,6 +1190,10 @@ class DatabaseConnector:
"""
table = model_class.__name__
fields = _get_model_fields(model_class)
fields["_createdAt"] = "DOUBLE PRECISION"
fields["_modifiedAt"] = "DOUBLE PRECISION"
fields["_createdBy"] = "TEXT"
fields["_modifiedBy"] = "TEXT"
if column not in fields:
return []

View file

@ -418,6 +418,8 @@ class AutomationObjects:
if not self.checkRbacPermission(AutomationDefinition, "update", automationId):
raise PermissionError(f"No permission to modify automation {automationId}")
automationData.pop("executionLogs", None)
# If deactivating: immediately remove scheduler job (don't rely on async callback)
isBeingDeactivated = "active" in automationData and not automationData["active"]
if isBeingDeactivated:

View file

@ -27,11 +27,6 @@ UI_OBJECTS = [
"label": {"en": "Templates", "de": "Vorlagen", "fr": "Modèles"},
"meta": {"area": "templates"}
},
{
"objectKey": "ui.feature.automation.logs",
"label": {"en": "Execution Logs", "de": "Ausführungsprotokolle", "fr": "Journaux d'exécution"},
"meta": {"area": "logs"}
},
]
# Resource Objects for RBAC catalog

View file

@ -785,6 +785,7 @@ def update_automation(
try:
chatInterface = getAutomationInterface(context.user, mandateId=str(context.mandateId) if context.mandateId else None)
automationData = automation.model_dump()
automationData.pop("executionLogs", None)
updated = chatInterface.updateAutomationDefinition(automationId, automationData)
return updated
except HTTPException:

View file

@ -0,0 +1,207 @@
# Copyright (c) 2025 Patrick Motsch
# All rights reserved.
"""
Admin automation execution logs routes.
SysAdmin-only endpoints for viewing consolidated automation execution history
across all mandates and feature instances.
"""
from fastapi import APIRouter, HTTPException, Depends, Request, Query
from typing import List, Dict, Any, Optional
import logging
import json
import math
import uuid
from modules.auth import limiter, requireSysAdminRole
from modules.datamodels.datamodelUam import User
from modules.datamodels.datamodelPagination import PaginationParams, PaginationMetadata, normalize_pagination_dict
from modules.routes.routeDataUsers import _applyFiltersAndSort, _extractDistinctValues
logger = logging.getLogger(__name__)
router = APIRouter(
prefix="/api/admin/automation-logs",
tags=["Admin Automation Logs"],
responses={
401: {"description": "Unauthorized"},
403: {"description": "Forbidden - Sysadmin only"},
500: {"description": "Internal server error"},
},
)
def _buildFlattenedExecutionLogs(currentUser: User) -> List[Dict[str, Any]]:
"""Flatten executionLogs from all AutomationDefinitions across all mandates.
Called from a SysAdmin-only endpoint bypasses RBAC, reads directly from DB."""
from modules.interfaces.interfaceDbApp import getRootInterface
from modules.features.automation.mainAutomation import getAutomationServices
from modules.features.automation.datamodelFeatureAutomation import AutomationDefinition
rootInterface = getRootInterface()
services = getAutomationServices(currentUser, mandateId=None, featureInstanceId=None)
allAutomations = services.interfaceDbAutomation.db.getRecordset(AutomationDefinition)
userCache: Dict[str, str] = {}
mandateCache: Dict[str, str] = {}
featureCache: Dict[str, str] = {}
def _resolveUsername(userId: str) -> str:
if not userId:
return ""
if userId not in userCache:
try:
user = rootInterface.getUser(userId)
userCache[userId] = user.username if user else userId[:8]
except Exception:
userCache[userId] = userId[:8]
return userCache[userId]
def _resolveMandateLabel(mandateId: str) -> str:
if not mandateId:
return ""
if mandateId not in mandateCache:
try:
mandate = rootInterface.getMandate(mandateId)
mandateCache[mandateId] = getattr(mandate, "label", None) or mandateId[:8]
except Exception:
mandateCache[mandateId] = mandateId[:8]
return mandateCache[mandateId]
def _resolveFeatureLabel(featureInstanceId: str) -> str:
if not featureInstanceId:
return ""
if featureInstanceId not in featureCache:
try:
instance = rootInterface.getFeatureInstance(featureInstanceId)
featureCache[featureInstanceId] = (
getattr(instance, "label", None)
or getattr(instance, "featureCode", None)
or featureInstanceId[:8]
)
except Exception:
featureCache[featureInstanceId] = featureInstanceId[:8]
return featureCache[featureInstanceId]
flatLogs: List[Dict[str, Any]] = []
for automation in allAutomations:
if isinstance(automation, dict):
automationId = automation.get("id", "")
automationLabel = automation.get("label", "")
mandateId = automation.get("mandateId", "")
featureInstanceId = automation.get("featureInstanceId", "")
createdBy = automation.get("_createdBy", "")
logs = automation.get("executionLogs") or []
else:
automationId = getattr(automation, "id", "")
automationLabel = getattr(automation, "label", "")
mandateId = getattr(automation, "mandateId", "")
featureInstanceId = getattr(automation, "featureInstanceId", "")
createdBy = getattr(automation, "_createdBy", "")
logs = getattr(automation, "executionLogs", None) or []
mandateName = _resolveMandateLabel(mandateId)
featureInstanceName = _resolveFeatureLabel(featureInstanceId)
executedByName = _resolveUsername(createdBy)
for log in logs:
timestamp = log.get("timestamp", 0) if isinstance(log, dict) else 0
status = log.get("status", "") if isinstance(log, dict) else ""
workflowId = log.get("workflowId", "") if isinstance(log, dict) else ""
messages = log.get("messages", []) if isinstance(log, dict) else []
flatLogs.append({
"id": str(uuid.uuid4()),
"timestamp": timestamp,
"automationId": automationId,
"automationLabel": automationLabel,
"mandateName": mandateName,
"featureInstanceName": featureInstanceName,
"executedBy": executedByName,
"status": status,
"workflowId": workflowId,
"messages": "; ".join(messages) if messages else "",
})
flatLogs.sort(key=lambda x: x.get("timestamp", 0), reverse=True)
return flatLogs
@router.get("")
@limiter.limit("30/minute")
def get_all_automation_logs(
request: Request,
pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams"),
currentUser: User = Depends(requireSysAdminRole),
):
"""Get consolidated execution logs from all automations (sysadmin only)."""
try:
paginationParams: Optional[PaginationParams] = None
if pagination:
try:
paginationDict = json.loads(pagination)
if paginationDict:
paginationDict = normalize_pagination_dict(paginationDict)
paginationParams = PaginationParams(**paginationDict)
except (json.JSONDecodeError, ValueError) as e:
raise HTTPException(status_code=400, detail=f"Invalid pagination parameter: {str(e)}")
logs = _buildFlattenedExecutionLogs(currentUser)
filtered = _applyFiltersAndSort(logs, paginationParams)
if paginationParams:
totalItems = len(filtered)
totalPages = math.ceil(totalItems / paginationParams.pageSize) if totalItems > 0 else 0
startIdx = (paginationParams.page - 1) * paginationParams.pageSize
endIdx = startIdx + paginationParams.pageSize
return {
"items": filtered[startIdx:endIdx],
"pagination": PaginationMetadata(
currentPage=paginationParams.page,
pageSize=paginationParams.pageSize,
totalItems=totalItems,
totalPages=totalPages,
sort=paginationParams.sort,
filters=paginationParams.filters,
).model_dump(),
}
return {"items": logs, "pagination": None}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error getting automation logs: {str(e)}")
raise HTTPException(status_code=500, detail=f"Error getting automation logs: {str(e)}")
@router.get("/filter-values")
@limiter.limit("60/minute")
def get_automation_log_filter_values(
request: Request,
column: str = Query(..., description="Column key"),
pagination: Optional[str] = Query(None, description="JSON-encoded current filters"),
currentUser: User = Depends(requireSysAdminRole),
):
"""Return distinct filter values for a column in automation logs."""
try:
crossFilterParams: Optional[PaginationParams] = None
if pagination:
try:
paginationDict = json.loads(pagination)
if paginationDict:
paginationDict = normalize_pagination_dict(paginationDict)
filters = paginationDict.get("filters", {})
filters.pop(column, None)
paginationDict["filters"] = filters
paginationDict.pop("sort", None)
crossFilterParams = PaginationParams(**paginationDict)
except (json.JSONDecodeError, ValueError):
pass
logs = _buildFlattenedExecutionLogs(currentUser)
crossFiltered = _applyFiltersAndSort(logs, crossFilterParams)
return _extractDistinctValues(crossFiltered, column)
except Exception as e:
logger.error(f"Error getting filter values: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))

View file

@ -258,6 +258,27 @@ def getModelAttributeDefinitions(modelClass: Type[BaseModel] = None, userLanguag
attributes.append(attr_def)
# Append system timestamp fields (set automatically by DatabaseConnector)
systemTimestampFields = [
("_createdAt", {"en": "Created at", "de": "Erstellt am", "fr": "Créé le"}),
("_modifiedAt", {"en": "Modified at", "de": "Geändert am", "fr": "Modifié le"}),
]
for sysName, sysLabels in systemTimestampFields:
attributes.append({
"name": sysName,
"type": "timestamp",
"required": False,
"description": "",
"label": sysLabels.get(userLanguage, sysLabels["en"]),
"placeholder": "",
"editable": False,
"visible": True,
"order": len(attributes),
"readonly": True,
"options": None,
"default": None,
})
return {"model": model_label, "attributes": attributes}

View file

@ -196,7 +196,7 @@ NAVIGATION_SECTIONS = [
"objectKey": "ui.admin.subscriptions",
"label": {"en": "Subscriptions", "de": "Abonnements", "fr": "Abonnements"},
"icon": "FaFileContract",
"path": "/admin/billing/subscriptions",
"path": "/admin/subscriptions",
"order": 50,
"adminOnly": True,
},
@ -282,6 +282,16 @@ NAVIGATION_SECTIONS = [
"adminOnly": True,
"sysAdminOnly": True,
},
{
"id": "admin-automation-logs",
"objectKey": "ui.admin.automationLogs",
"label": {"en": "Execution Logs", "de": "Ausführungsprotokolle", "fr": "Journaux d'exécution"},
"icon": "FaClipboardList",
"path": "/admin/automation-logs",
"order": 85,
"adminOnly": True,
"sysAdminOnly": True,
},
{
"id": "admin-logs",
"objectKey": "ui.admin.logs",