grapheditor editor and template handling
This commit is contained in:
parent
f65223137e
commit
13242fa5ac
8 changed files with 244 additions and 58 deletions
13
app.py
13
app.py
|
|
@ -244,6 +244,8 @@ def initLogging():
|
|||
"fastapi.security.oauth2",
|
||||
"msal",
|
||||
"azure.core.pipeline.policies.http_logging_policy",
|
||||
"stripe",
|
||||
"apscheduler",
|
||||
]
|
||||
for loggerName in noisyLoggers:
|
||||
logging.getLogger(loggerName).setLevel(logging.WARNING)
|
||||
|
|
@ -294,16 +296,7 @@ except Exception as e:
|
|||
async def lifespan(app: FastAPI):
|
||||
logger.info("Application is starting up")
|
||||
|
||||
# --- Pre-warm AI connectors FIRST (before any other startup work) ---
|
||||
# Avoids 4–8 s latency on first chatbot request; must run before first use.
|
||||
try:
|
||||
import modules.aicore.aicoreModelRegistry # noqa: F401 - triggers eager pre-warm
|
||||
from modules.aicore.aicoreModelRegistry import modelRegistry
|
||||
modelRegistry.ensureConnectorsRegistered()
|
||||
modelRegistry.refreshModels(force=True)
|
||||
logger.info("AI connectors and model registry pre-warmed")
|
||||
except Exception as e:
|
||||
logger.warning(f"AI pre-warm failed: {e}")
|
||||
# AI connectors already pre-warmed at module-load via _eager_prewarm() in aicoreModelRegistry.
|
||||
|
||||
# Bootstrap database if needed (creates initial users, mandates, roles, etc.)
|
||||
# This must happen before getting root interface
|
||||
|
|
|
|||
|
|
@ -556,7 +556,9 @@ class GraphicalEditorObjects:
|
|||
# -------------------------------------------------------------------------
|
||||
|
||||
def getTemplates(self, scope: str = None) -> List[Dict[str, Any]]:
|
||||
"""Get workflow templates, optionally filtered by scope."""
|
||||
"""Get workflow templates, optionally filtered by scope.
|
||||
Always includes system-scope templates (mandateId=None) alongside mandate-owned ones.
|
||||
"""
|
||||
if not self.db._ensureTableExists(AutoWorkflow):
|
||||
return []
|
||||
rf: Dict[str, Any] = {
|
||||
|
|
@ -566,7 +568,21 @@ class GraphicalEditorObjects:
|
|||
}
|
||||
if scope:
|
||||
rf["templateScope"] = scope
|
||||
records = self.db.getRecordset(AutoWorkflow, recordFilter=rf)
|
||||
records = self.db.getRecordset(AutoWorkflow, recordFilter=rf) or []
|
||||
|
||||
if scope is None or scope == "system":
|
||||
systemFilter: Dict[str, Any] = {
|
||||
"isTemplate": True,
|
||||
"templateScope": "system",
|
||||
"mandateId": None,
|
||||
}
|
||||
systemRecords = self.db.getRecordset(AutoWorkflow, recordFilter=systemFilter) or []
|
||||
seenIds = {(r.get("id") if isinstance(r, dict) else getattr(r, "id", None)) for r in records}
|
||||
for sr in systemRecords:
|
||||
srId = sr.get("id") if isinstance(sr, dict) else getattr(sr, "id", None)
|
||||
if srId not in seenIds:
|
||||
records.append(sr)
|
||||
|
||||
return [dict(r) for r in records] if records else []
|
||||
|
||||
def createTemplateFromWorkflow(self, workflowId: str, scope: str = "user") -> Optional[Dict[str, Any]]:
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ from .email import EMAIL_NODES
|
|||
from .sharepoint import SHAREPOINT_NODES
|
||||
from .clickup import CLICKUP_NODES
|
||||
from .file import FILE_NODES
|
||||
from .trustee import TRUSTEE_NODES
|
||||
|
||||
STATIC_NODE_TYPES = (
|
||||
TRIGGER_NODES
|
||||
|
|
@ -19,4 +20,5 @@ STATIC_NODE_TYPES = (
|
|||
+ SHAREPOINT_NODES
|
||||
+ CLICKUP_NODES
|
||||
+ FILE_NODES
|
||||
+ TRUSTEE_NODES
|
||||
)
|
||||
|
|
|
|||
68
modules/features/graphicalEditor/nodeDefinitions/trustee.py
Normal file
68
modules/features/graphicalEditor/nodeDefinitions/trustee.py
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
# Copyright (c) 2025 Patrick Motsch
|
||||
# Trustee node definitions - map to methodTrustee actions.
|
||||
# Pipeline: extractFromFiles -> processDocuments -> syncToAccounting.
|
||||
|
||||
TRUSTEE_NODES = [
|
||||
{
|
||||
"id": "trustee.extractFromFiles",
|
||||
"category": "trustee",
|
||||
"label": {"en": "Extract Documents", "de": "Dokumente extrahieren", "fr": "Extraire documents"},
|
||||
"description": {
|
||||
"en": "Extract document type and data from PDF/JPG via AI (from fileIds or SharePoint folder)",
|
||||
"de": "Dokumenttyp und Daten aus PDF/JPG per AI extrahieren (aus Dateien oder SharePoint-Ordner)",
|
||||
"fr": "Extraire type et données de PDF/JPG par IA",
|
||||
},
|
||||
"parameters": [
|
||||
{"name": "connectionId", "type": "string", "required": False, "description": {"en": "SharePoint connection (if reading from SharePoint)", "de": "SharePoint-Verbindung (falls aus SharePoint)", "fr": "Connexion SharePoint"}, "default": ""},
|
||||
{"name": "sharepointFolder", "type": "string", "required": False, "description": {"en": "SharePoint folder path (e.g. /sites/MySite/Documents/Expenses)", "de": "SharePoint-Ordnerpfad", "fr": "Chemin dossier SharePoint"}, "default": ""},
|
||||
{"name": "featureInstanceId", "type": "string", "required": True, "description": {"en": "Trustee feature instance ID", "de": "Trustee Feature-Instanz-ID", "fr": "ID instance Trustee"}},
|
||||
{"name": "prompt", "type": "string", "required": False, "description": {"en": "AI prompt for extraction (optional)", "de": "AI-Prompt für Extraktion (optional)", "fr": "Prompt IA pour extraction"}, "default": ""},
|
||||
],
|
||||
"inputs": 1,
|
||||
"outputs": 1,
|
||||
"meta": {"icon": "mdi-file-document-scan", "color": "#4CAF50"},
|
||||
"_method": "trustee",
|
||||
"_action": "extractFromFiles",
|
||||
"_paramMap": {"connectionId": "connectionReference", "sharepointFolder": "sharepointFolder", "featureInstanceId": "featureInstanceId", "prompt": "prompt"},
|
||||
},
|
||||
{
|
||||
"id": "trustee.processDocuments",
|
||||
"category": "trustee",
|
||||
"label": {"en": "Process Documents", "de": "Dokumente verarbeiten", "fr": "Traiter documents"},
|
||||
"description": {
|
||||
"en": "Create TrusteeDocument + TrusteePosition from extraction result",
|
||||
"de": "TrusteeDocument + TrusteePosition aus Extraktionsergebnis erstellen",
|
||||
"fr": "Créer TrusteeDocument + TrusteePosition à partir du résultat",
|
||||
},
|
||||
"parameters": [
|
||||
{"name": "documentList", "type": "string", "required": True, "description": {"en": "Reference to extractFromFiles result", "de": "Referenz auf extractFromFiles-Ergebnis", "fr": "Référence au résultat extractFromFiles"}},
|
||||
{"name": "featureInstanceId", "type": "string", "required": True, "description": {"en": "Trustee feature instance ID", "de": "Trustee Feature-Instanz-ID", "fr": "ID instance Trustee"}},
|
||||
],
|
||||
"inputs": 1,
|
||||
"outputs": 1,
|
||||
"meta": {"icon": "mdi-file-document-check", "color": "#4CAF50"},
|
||||
"_method": "trustee",
|
||||
"_action": "processDocuments",
|
||||
"_paramMap": {"documentList": "documentList", "featureInstanceId": "featureInstanceId"},
|
||||
},
|
||||
{
|
||||
"id": "trustee.syncToAccounting",
|
||||
"category": "trustee",
|
||||
"label": {"en": "Sync to Accounting", "de": "In Buchhaltung synchronisieren", "fr": "Synchroniser comptabilité"},
|
||||
"description": {
|
||||
"en": "Push trustee positions to accounting system",
|
||||
"de": "Trustee-Positionen in Buchhaltungssystem übertragen",
|
||||
"fr": "Transférer les positions vers la comptabilité",
|
||||
},
|
||||
"parameters": [
|
||||
{"name": "documentList", "type": "string", "required": True, "description": {"en": "Reference to processDocuments result", "de": "Referenz auf processDocuments-Ergebnis", "fr": "Référence au résultat processDocuments"}},
|
||||
{"name": "featureInstanceId", "type": "string", "required": True, "description": {"en": "Trustee feature instance ID", "de": "Trustee Feature-Instanz-ID", "fr": "ID instance Trustee"}},
|
||||
],
|
||||
"inputs": 1,
|
||||
"outputs": 1,
|
||||
"meta": {"icon": "mdi-calculator", "color": "#4CAF50"},
|
||||
"_method": "trustee",
|
||||
"_action": "syncToAccounting",
|
||||
"_paramMap": {"documentList": "documentList", "featureInstanceId": "featureInstanceId"},
|
||||
},
|
||||
]
|
||||
|
|
@ -470,6 +470,9 @@ async def post_editor_chat(
|
|||
|
||||
userLanguage = body.get("userLanguage", "de")
|
||||
conversationHistory = body.get("conversationHistory") or []
|
||||
fileIds = body.get("fileIds") or []
|
||||
dataSourceIds = body.get("dataSourceIds") or []
|
||||
featureDataSourceIds = body.get("featureDataSourceIds") or []
|
||||
|
||||
from modules.serviceCenter.core.serviceStreaming import get_event_manager
|
||||
sseEventManager = get_event_manager()
|
||||
|
|
@ -487,6 +490,9 @@ async def post_editor_chat(
|
|||
sseEventManager=sseEventManager,
|
||||
userLanguage=userLanguage,
|
||||
conversationHistory=conversationHistory,
|
||||
fileIds=fileIds,
|
||||
dataSourceIds=dataSourceIds,
|
||||
featureDataSourceIds=featureDataSourceIds,
|
||||
)
|
||||
)
|
||||
sseEventManager.register_agent_task(queueId, agentTask)
|
||||
|
|
@ -531,6 +537,9 @@ async def _runEditorAgent(
|
|||
sseEventManager=None,
|
||||
userLanguage: str = "de",
|
||||
conversationHistory: List[Dict[str, Any]] = None,
|
||||
fileIds: List[str] = None,
|
||||
dataSourceIds: List[str] = None,
|
||||
featureDataSourceIds: List[str] = None,
|
||||
):
|
||||
"""Run the serviceAgent loop with workflow toolbox and forward events to the SSE queue."""
|
||||
try:
|
||||
|
|
@ -555,10 +564,25 @@ async def _runEditorAgent(
|
|||
"Respond concisely and confirm what you changed."
|
||||
)
|
||||
|
||||
enrichedPrompt = prompt
|
||||
if dataSourceIds:
|
||||
from modules.features.workspace.routeFeatureWorkspace import _buildDataSourceContext
|
||||
chatSvc = getService("chat", ctx)
|
||||
dsInfo = _buildDataSourceContext(chatSvc, dataSourceIds)
|
||||
if dsInfo:
|
||||
enrichedPrompt = f"{prompt}\n\n[Active Data Sources]\n{dsInfo}"
|
||||
|
||||
if featureDataSourceIds:
|
||||
from modules.features.workspace.routeFeatureWorkspace import _buildFeatureDataSourceContext
|
||||
fdsInfo = _buildFeatureDataSourceContext(featureDataSourceIds)
|
||||
if fdsInfo:
|
||||
enrichedPrompt = f"{enrichedPrompt}\n\n[Attached Feature Data Sources]\n{fdsInfo}"
|
||||
|
||||
accumulatedText = ""
|
||||
|
||||
async for event in agentService.runAgent(
|
||||
prompt=prompt,
|
||||
prompt=enrichedPrompt,
|
||||
fileIds=fileIds or [],
|
||||
workflowId=workflowId,
|
||||
userLanguage=userLanguage,
|
||||
conversationHistory=conversationHistory or [],
|
||||
|
|
|
|||
|
|
@ -1221,17 +1221,15 @@ async def listWorkspaceDataSources(
|
|||
instanceId: str = Path(...),
|
||||
context: RequestContext = Depends(getRequestContext),
|
||||
):
|
||||
_validateInstanceAccess(instanceId, context)
|
||||
wsMandateId, _ = _validateInstanceAccess(instanceId, context)
|
||||
try:
|
||||
from modules.serviceCenter import getService
|
||||
from modules.serviceCenter.context import ServiceCenterContext
|
||||
ctx = ServiceCenterContext(
|
||||
user=context.user,
|
||||
mandate_id=str(context.mandateId) if context.mandateId else None,
|
||||
feature_instance_id=instanceId,
|
||||
)
|
||||
chatService = getService("chat", ctx)
|
||||
dataSources = chatService.listDataSources(featureInstanceId=instanceId)
|
||||
from modules.datamodels.datamodelDataSource import DataSource
|
||||
from modules.interfaces.interfaceDbApp import getRootInterface
|
||||
rootIf = getRootInterface()
|
||||
recordFilter: dict = {"featureInstanceId": instanceId}
|
||||
if wsMandateId:
|
||||
recordFilter["mandateId"] = wsMandateId
|
||||
dataSources = rootIf.db.getRecordset(DataSource, recordFilter=recordFilter)
|
||||
return JSONResponse({"dataSources": dataSources or []})
|
||||
except Exception:
|
||||
return JSONResponse({"dataSources": []})
|
||||
|
|
@ -1642,16 +1640,16 @@ async def listFeatureDataSources(
|
|||
instanceId: str = Path(...),
|
||||
context: RequestContext = Depends(getRequestContext),
|
||||
):
|
||||
"""List active FeatureDataSources for this workspace instance."""
|
||||
_validateInstanceAccess(instanceId, context)
|
||||
"""List active FeatureDataSources for this workspace instance, scoped to mandate."""
|
||||
wsMandateId, _ = _validateInstanceAccess(instanceId, context)
|
||||
from modules.interfaces.interfaceDbApp import getRootInterface
|
||||
from modules.datamodels.datamodelFeatureDataSource import FeatureDataSource
|
||||
|
||||
rootIf = getRootInterface()
|
||||
records = rootIf.db.getRecordset(
|
||||
FeatureDataSource,
|
||||
recordFilter={"workspaceInstanceId": instanceId},
|
||||
)
|
||||
recordFilter: dict = {"workspaceInstanceId": instanceId}
|
||||
if wsMandateId:
|
||||
recordFilter["mandateId"] = wsMandateId
|
||||
records = rootIf.db.getRecordset(FeatureDataSource, recordFilter=recordFilter)
|
||||
return JSONResponse({"featureDataSources": records or []})
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -38,13 +38,21 @@ pwdContext = CryptContext(schemes=["argon2"], deprecated="auto")
|
|||
# Cache für Role-IDs (roleLabel -> roleId)
|
||||
_roleIdCache: Dict[str, str] = {}
|
||||
|
||||
_bootstrapDone: bool = False
|
||||
|
||||
def initBootstrap(db: DatabaseConnector) -> None:
|
||||
"""
|
||||
Main bootstrap entry point - initializes all system components.
|
||||
Idempotent: runs only once per process regardless of how many callers invoke it.
|
||||
|
||||
Args:
|
||||
db: Database connector instance
|
||||
"""
|
||||
global _bootstrapDone
|
||||
if _bootstrapDone:
|
||||
return
|
||||
_bootstrapDone = True
|
||||
|
||||
logger.info("Starting system bootstrap")
|
||||
|
||||
# Initialize root mandate
|
||||
|
|
@ -144,6 +152,112 @@ def initBootstrap(db: DatabaseConnector) -> None:
|
|||
except Exception as e:
|
||||
logger.warning(f"Mandate retention purge failed: {e}")
|
||||
|
||||
# Bootstrap system workflow templates for graphical editor
|
||||
_bootstrapSystemTemplates(db)
|
||||
|
||||
|
||||
def _bootstrapSystemTemplates(db: DatabaseConnector) -> None:
|
||||
"""
|
||||
Seed platform-wide workflow templates (templateScope='system', mandateId=None).
|
||||
Idempotent: skips if templates with the same label already exist.
|
||||
"""
|
||||
try:
|
||||
from modules.features.graphicalEditor.datamodelFeatureGraphicalEditor import AutoWorkflow
|
||||
import uuid
|
||||
|
||||
greenfieldDb = DatabaseConnector(
|
||||
dbHost=APP_CONFIG.get("DB_HOST", "localhost"),
|
||||
dbDatabase="poweron_graphicaleditor",
|
||||
dbUser=APP_CONFIG.get("DB_USER"),
|
||||
dbPassword=APP_CONFIG.get("DB_PASSWORD_SECRET") or APP_CONFIG.get("DB_PASSWORD"),
|
||||
)
|
||||
greenfieldDb._ensureTableExists(AutoWorkflow)
|
||||
|
||||
existing = greenfieldDb.getRecordset(AutoWorkflow, recordFilter={
|
||||
"isTemplate": True,
|
||||
"templateScope": "system",
|
||||
})
|
||||
existingLabels = {r.get("label") if isinstance(r, dict) else getattr(r, "label", "") for r in (existing or [])}
|
||||
|
||||
templates = _buildSystemTemplates()
|
||||
created = 0
|
||||
for tpl in templates:
|
||||
if tpl["label"] in existingLabels:
|
||||
continue
|
||||
tpl["id"] = str(uuid.uuid4())
|
||||
greenfieldDb.recordCreate(AutoWorkflow, tpl)
|
||||
created += 1
|
||||
|
||||
if created:
|
||||
logger.info(f"Bootstrapped {created} system workflow template(s)")
|
||||
greenfieldDb.close()
|
||||
except Exception as e:
|
||||
logger.warning(f"System workflow template bootstrap failed: {e}")
|
||||
|
||||
|
||||
def _buildSystemTemplates():
|
||||
"""Build the graph definitions for platform system templates."""
|
||||
return [
|
||||
{
|
||||
"label": "Personal Assistant: E-Mail-Antwort-Drafting",
|
||||
"mandateId": None,
|
||||
"featureInstanceId": None,
|
||||
"isTemplate": True,
|
||||
"templateScope": "system",
|
||||
"sharedReadOnly": True,
|
||||
"active": False,
|
||||
"graph": {
|
||||
"nodes": [
|
||||
{"id": "n1", "type": "trigger.schedule", "x": 50, "y": 200, "title": "Täglicher Check", "parameters": {}},
|
||||
{"id": "n2", "type": "email.checkEmail", "x": 300, "y": 200, "title": "Mailbox prüfen", "parameters": {}},
|
||||
{"id": "n3", "type": "flow.loop", "x": 550, "y": 200, "title": "Pro E-Mail", "parameters": {}},
|
||||
{"id": "n4", "type": "ai.prompt", "x": 800, "y": 200, "title": "Analyse: Antwort nötig?", "parameters": {}},
|
||||
{"id": "n5", "type": "flow.ifElse", "x": 1050, "y": 200, "title": "Antwort nötig?", "parameters": {}},
|
||||
{"id": "n6", "type": "ai.prompt", "x": 1300, "y": 100, "title": "Kontext abrufen & Antwort formulieren", "parameters": {}},
|
||||
{"id": "n7", "type": "email.draftEmail", "x": 1550, "y": 100, "title": "Draft erstellen", "parameters": {}},
|
||||
],
|
||||
"connections": [
|
||||
{"source": "n1", "target": "n2", "sourceOutput": 0, "targetInput": 0},
|
||||
{"source": "n2", "target": "n3", "sourceOutput": 0, "targetInput": 0},
|
||||
{"source": "n3", "target": "n4", "sourceOutput": 0, "targetInput": 0},
|
||||
{"source": "n4", "target": "n5", "sourceOutput": 0, "targetInput": 0},
|
||||
{"source": "n5", "target": "n6", "sourceOutput": 0, "targetInput": 0},
|
||||
{"source": "n6", "target": "n7", "sourceOutput": 0, "targetInput": 0},
|
||||
],
|
||||
},
|
||||
"invocations": [{"type": "schedule", "cronExpression": "0 8 * * 1-5"}],
|
||||
},
|
||||
{
|
||||
"label": "Treuhand: PDF-Klassifizierung & Trustee-Import",
|
||||
"mandateId": None,
|
||||
"featureInstanceId": None,
|
||||
"isTemplate": True,
|
||||
"templateScope": "system",
|
||||
"sharedReadOnly": True,
|
||||
"active": False,
|
||||
"graph": {
|
||||
"nodes": [
|
||||
{"id": "n1", "type": "trigger.schedule", "x": 50, "y": 200, "title": "Geplanter Import", "parameters": {}},
|
||||
{"id": "n2", "type": "sharepoint.listFiles", "x": 300, "y": 200, "title": "SharePoint Ordner lesen", "parameters": {}},
|
||||
{"id": "n3", "type": "flow.loop", "x": 550, "y": 200, "title": "Pro Dokument", "parameters": {}},
|
||||
{"id": "n4", "type": "sharepoint.readFile", "x": 800, "y": 200, "title": "PDF-Inhalt lesen", "parameters": {}},
|
||||
{"id": "n5", "type": "ai.prompt", "x": 1050, "y": 200, "title": "Typ klassifizieren (Rechnung, Beleg, Bankauszug, Vertrag, etc.)", "parameters": {}},
|
||||
{"id": "n6", "type": "trustee.extractFromFiles", "x": 1300, "y": 200, "title": "Dokument extrahieren", "parameters": {}},
|
||||
{"id": "n7", "type": "trustee.processDocuments", "x": 1550, "y": 200, "title": "In Trustee einlesen", "parameters": {}},
|
||||
],
|
||||
"connections": [
|
||||
{"source": "n1", "target": "n2", "sourceOutput": 0, "targetInput": 0},
|
||||
{"source": "n2", "target": "n3", "sourceOutput": 0, "targetInput": 0},
|
||||
{"source": "n3", "target": "n4", "sourceOutput": 0, "targetInput": 0},
|
||||
{"source": "n4", "target": "n5", "sourceOutput": 0, "targetInput": 0},
|
||||
{"source": "n5", "target": "n6", "sourceOutput": 0, "targetInput": 0},
|
||||
{"source": "n6", "target": "n7", "sourceOutput": 0, "targetInput": 0},
|
||||
],
|
||||
},
|
||||
"invocations": [{"type": "schedule", "cronExpression": "0 7 * * 1-5"}],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def initRootMandateFeatures(db: DatabaseConnector, mandateId: str) -> None:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ import uuid
|
|||
from modules.connectors.connectorDbPostgre import DatabaseConnector, _get_cached_connector
|
||||
from modules.shared.configuration import APP_CONFIG
|
||||
from modules.shared.timeUtils import getUtcTimestamp, parseTimestamp
|
||||
from modules.interfaces.interfaceBootstrap import initBootstrap
|
||||
from modules.interfaces.interfaceRbac import getRecordsetWithRBAC
|
||||
from modules.security.rbac import RbacClass
|
||||
from modules.datamodels.datamodelUam import (
|
||||
|
|
@ -55,8 +54,6 @@ _gatewayInterfaces = {}
|
|||
# Root interface instance
|
||||
_rootAppObjects = None
|
||||
|
||||
# Bootstrap completion flag - ensures bootstrap runs only ONCE per application lifecycle
|
||||
_bootstrapCompleted = False
|
||||
|
||||
# Password-Hashing
|
||||
pwdContext = CryptContext(schemes=["argon2"], deprecated="auto")
|
||||
|
|
@ -79,9 +76,6 @@ class AppObjects:
|
|||
# Initialize database
|
||||
self._initializeDatabase()
|
||||
|
||||
# Initialize standard records if needed
|
||||
self._initRecords()
|
||||
|
||||
# Set user context if provided
|
||||
if currentUser:
|
||||
self.setUserContext(currentUser)
|
||||
|
|
@ -195,29 +189,6 @@ class AppObjects:
|
|||
|
||||
return simpleFields, objectFields
|
||||
|
||||
def _initRecords(self):
|
||||
"""Initialize standard records if they don't exist.
|
||||
|
||||
Uses a global flag to ensure bootstrap only runs ONCE per application lifecycle.
|
||||
The flag is set BEFORE calling bootstrap to prevent recursive calls during bootstrap.
|
||||
"""
|
||||
global _bootstrapCompleted
|
||||
|
||||
if _bootstrapCompleted:
|
||||
return
|
||||
|
||||
# Set flag BEFORE bootstrap to prevent recursive calls during bootstrap
|
||||
_bootstrapCompleted = True
|
||||
logger.info("Starting bootstrap (will only run once)")
|
||||
|
||||
try:
|
||||
initBootstrap(self.db)
|
||||
logger.info("Bootstrap completed successfully")
|
||||
except Exception as e:
|
||||
# Reset flag on failure so bootstrap can be retried
|
||||
_bootstrapCompleted = False
|
||||
logger.error(f"Bootstrap failed: {e}")
|
||||
raise
|
||||
|
||||
|
||||
def checkRbacPermission(
|
||||
|
|
|
|||
Loading…
Reference in a new issue