fix db sync
All checks were successful
Deploy Plattform-Core / test (push) Successful in 50s
Deploy Plattform-Core / deploy (push) Successful in 4s

This commit is contained in:
ValueOn AG 2026-05-27 17:43:48 +02:00
parent 6ab51cf67e
commit 2d796a34ed
3 changed files with 35 additions and 69 deletions

View file

@ -4,7 +4,7 @@
APP_ENV_TYPE = dev APP_ENV_TYPE = dev
APP_ENV_LABEL = Development Instance Patrick APP_ENV_LABEL = Development Instance Patrick
APP_API_URL = http://localhost:8000 APP_API_URL = http://localhost:8000
APP_KEY_SYSVAR = D:/Athi/Local/Web/poweron/local/notes/key.txt APP_KEY_SYSVAR = D:/Athi/Local/Web/poweron-swiss/local/notes/key.txt
APP_INIT_PASS_ADMIN_SECRET = DEV_ENC:Z0FBQUFBQm8xSUpEeFFtRGtQeVUtcjlrU3dab1ZxUm9WSks0MlJVYUtERFlqUElHemZrOGNENk1tcmJNX3Vxc01UMDhlNU40VzZZRVBpUGNmT3podzZrOGhOeEJIUEt4eVlSWG5UYXA3d09DVXlLT21Kb1JYSUU9 APP_INIT_PASS_ADMIN_SECRET = DEV_ENC:Z0FBQUFBQm8xSUpEeFFtRGtQeVUtcjlrU3dab1ZxUm9WSks0MlJVYUtERFlqUElHemZrOGNENk1tcmJNX3Vxc01UMDhlNU40VzZZRVBpUGNmT3podzZrOGhOeEJIUEt4eVlSWG5UYXA3d09DVXlLT21Kb1JYSUU9
APP_INIT_PASS_EVENT_SECRET = DEV_ENC:Z0FBQUFBQm8xSUpERzZjNm56WGVBdjJTeG5Udjd6OGQwUVotYXUzQjJ1YVNyVXVBa3NZVml3ODU0MVNkZjhWWmJwNUFkc19BcHlHMTU1Q3BRcHU0cDBoZkFlR2l6UEZQU3d2U3MtMDh5UDZteGFoQ0EyMUE1ckE9 APP_INIT_PASS_EVENT_SECRET = DEV_ENC:Z0FBQUFBQm8xSUpERzZjNm56WGVBdjJTeG5Udjd6OGQwUVotYXUzQjJ1YVNyVXVBa3NZVml3ODU0MVNkZjhWWmJwNUFkc19BcHlHMTU1Q3BRcHU0cDBoZkFlR2l6UEZQU3d2U3MtMDh5UDZteGFoQ0EyMUE1ckE9
@ -23,7 +23,7 @@ APP_ALLOWED_ORIGINS=http://localhost:8080,http://localhost:5176,https://nyla.pow
# Logging configuration # Logging configuration
APP_LOGGING_LOG_LEVEL = DEBUG APP_LOGGING_LOG_LEVEL = DEBUG
APP_LOGGING_LOG_DIR = D:/Athi/Local/Web/poweron/local/logs APP_LOGGING_LOG_DIR = D:/Athi/Local/Web/poweron-swiss/local/logs
APP_LOGGING_FORMAT = %(asctime)s - %(levelname)s - %(name)s - %(message)s APP_LOGGING_FORMAT = %(asctime)s - %(levelname)s - %(name)s - %(message)s
APP_LOGGING_DATE_FORMAT = %Y-%m-%d %H:%M:%S APP_LOGGING_DATE_FORMAT = %Y-%m-%d %H:%M:%S
APP_LOGGING_CONSOLE_ENABLED = True APP_LOGGING_CONSOLE_ENABLED = True
@ -80,9 +80,9 @@ TEAMSBOT_BROWSER_BOT_URL = http://localhost:4100
# Debug Configuration # Debug Configuration
APP_DEBUG_CHAT_WORKFLOW_ENABLED = True APP_DEBUG_CHAT_WORKFLOW_ENABLED = True
APP_DEBUG_CHAT_WORKFLOW_DIR = D:/Athi/Local/Web/poweron/local/debug APP_DEBUG_CHAT_WORKFLOW_DIR = D:/Athi/Local/Web/poweron-swiss/local/debug
APP_DEBUG_ACCOUNTING_SYNC_ENABLED = True APP_DEBUG_ACCOUNTING_SYNC_ENABLED = True
APP_DEBUG_ACCOUNTING_SYNC_DIR = D:/Athi/Local/Web/poweron/local/debug/sync APP_DEBUG_ACCOUNTING_SYNC_DIR = D:/Athi/Local/Web/poweron-swiss/local/debug/sync
# Azure Communication Services Email Configuration # Azure Communication Services Email Configuration
MESSAGING_ACS_CONNECTION_STRING = endpoint=https://mailing-poweron-prod.switzerland.communication.azure.com/;accesskey=4UizRfBKBgMhDgQ92IYINM6dJsO1HIeL6W1DvIX9S0GtaS1PjIXqJQQJ99CAACULyCpHwxUcAAAAAZCSuSCt MESSAGING_ACS_CONNECTION_STRING = endpoint=https://mailing-poweron-prod.switzerland.communication.azure.com/;accesskey=4UizRfBKBgMhDgQ92IYINM6dJsO1HIeL6W1DvIX9S0GtaS1PjIXqJQQJ99CAACULyCpHwxUcAAAAAZCSuSCt

View file

@ -438,38 +438,18 @@ async def postMigrationImport(
# Per-DB endpoints (progress-friendly) # Per-DB endpoints (progress-friendly)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
_pendingExports: Dict[str, dict] = {}
@router.post("/migration/export-start")
@limiter.limit("10/minute")
def postMigrationExportStart(
request: Request,
currentUser: User = Depends(requireSysAdmin),
) -> Dict[str, Any]:
"""Start an export session. Returns a token for subsequent per-DB calls."""
import uuid
token = str(uuid.uuid4())
_pendingExports[token] = {"databases": {}}
logger.info("SysAdmin migration export-start: user=%s token=%s", currentUser.username, token)
return {"token": token}
@router.get("/migration/export-single") @router.get("/migration/export-single")
@limiter.limit("60/minute") @limiter.limit("60/minute")
def getMigrationExportSingle( def getMigrationExportSingle(
request: Request, request: Request,
token: str,
database: str, database: str,
currentUser: User = Depends(requireSysAdmin), currentUser: User = Depends(requireSysAdmin),
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Export a single database and store it server-side. Returns only metadata.""" """Export a single database. Returns full payload so the frontend can
assemble the final JSON client-side (no server-side state needed)."""
from modules.shared.dbRegistry import getRegisteredDatabases from modules.shared.dbRegistry import getRegisteredDatabases
pending = _pendingExports.get(token)
if not pending:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid export token.")
if database not in getRegisteredDatabases(): if database not in getRegisteredDatabases():
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
@ -487,7 +467,6 @@ def getMigrationExportSingle(
detail=f"Export failed for '{database}': {e}", detail=f"Export failed for '{database}': {e}",
) from e ) from e
pending["databases"][database] = dbPayload
logger.info("SysAdmin migration export-single done: user=%s db=%s tables=%s records=%s", logger.info("SysAdmin migration export-single done: user=%s db=%s tables=%s records=%s",
currentUser.username, database, dbPayload.get("tableCount", 0), dbPayload.get("totalRecords", 0)) currentUser.username, database, dbPayload.get("tableCount", 0), dbPayload.get("totalRecords", 0))
@ -495,51 +474,10 @@ def getMigrationExportSingle(
"database": database, "database": database,
"tableCount": dbPayload.get("tableCount", 0), "tableCount": dbPayload.get("tableCount", 0),
"totalRecords": dbPayload.get("totalRecords", 0), "totalRecords": dbPayload.get("totalRecords", 0),
"payload": dbPayload,
} }
@router.get("/migration/export-download")
@limiter.limit("5/minute")
def getMigrationExportDownload(
request: Request,
token: str,
filename: str = "backup.json",
currentUser: User = Depends(requireSysAdmin),
) -> StreamingResponse:
"""Assemble and stream the final export file from server-side data."""
from datetime import datetime, timezone
pending = _pendingExports.pop(token, None)
if not pending or not pending.get("databases"):
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid or expired export token.")
databases = pending["databases"]
totalTables = sum(d.get("tableCount", 0) for d in databases.values())
totalRecords = sum(d.get("totalRecords", 0) for d in databases.values())
exportData = {
"meta": {
"exportedAt": datetime.now(timezone.utc).isoformat(),
"version": "1.0",
"databaseCount": len(databases),
"totalTables": totalTables,
"totalRecords": totalRecords,
},
"databases": databases,
}
logger.info("SysAdmin migration export-download: user=%s dbs=%s tables=%s records=%s",
currentUser.username, len(databases), totalTables, totalRecords)
content = json.dumps(exportData, ensure_ascii=False, default=str)
return StreamingResponse(
iter([content]),
media_type="application/json",
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
)
def _processUploadedFile(filePath: str, tmpDir: str, token: str) -> dict: def _processUploadedFile(filePath: str, tmpDir: str, token: str) -> dict:
"""Parse JSON, validate, remap, split into per-DB files. """Parse JSON, validate, remap, split into per-DB files.

View file

@ -298,6 +298,7 @@ def get_folder_tree(
folders = managementInterface.getSharedFolderTree() folders = managementInterface.getSharedFolderTree()
else: else:
raise HTTPException(status_code=400, detail="owner must be 'me' or 'shared'") raise HTTPException(status_code=400, detail="owner must be 'me' or 'shared'")
_enrichFoldersWithMixed(managementInterface.db, str(currentUser.id), folders, o)
return folders return folders
except HTTPException: except HTTPException:
raise raise
@ -372,6 +373,33 @@ def getAttributesForIds(
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
def _enrichFoldersWithMixed(
db, userId: str, folders: List[Dict[str, Any]], ownerMode: str,
) -> None:
"""Enrich folder dicts in-place: replace raw neutralize/scope with
computed values that include ``'mixed'`` when children diverge.
For ``ownerMode='me'``, files owned by the user are loaded.
For ``'shared'``, files inside the visible shared folders are loaded."""
if not folders:
return
if ownerMode == "me":
allFiles = db.getRecordset(FileItem, recordFilter={"sysCreatedBy": userId}) or []
else:
folderIds = {f["id"] for f in folders}
allFiles = []
for fid in folderIds:
allFiles.extend(db.getRecordset(FileItem, recordFilter={"folderId": fid}) or [])
computed: Dict[str, Dict[str, Any]] = {}
for folder in folders:
computed[folder["id"]] = _computeFolderAttrs(folder, folders, allFiles)
for folder in folders:
attrs = computed[folder["id"]]
folder["neutralize"] = attrs["neutralize"]
folder["scope"] = attrs["scope"]
def _computeFolderAttrs( def _computeFolderAttrs(
folder: Dict[str, Any], folder: Dict[str, Any],
allFolders: List[Dict[str, Any]], allFolders: List[Dict[str, Any]],