Add AI parsing and graph parsing status; update related hooks and localization; modify microtime values in config and state files

This commit is contained in:
2026-03-27 10:47:30 +01:00
parent fa3c92379f
commit 412c25f184
14 changed files with 164 additions and 15 deletions

View File

@@ -29,9 +29,7 @@ class UpdateLastSyncFromDocuments implements BeforeSave
$pdo = $this->entityManager->getPDO(); $pdo = $this->entityManager->getPDO();
$aktenId = $entity->getId(); $aktenId = $entity->getId();
// Einzelne Zeile mit aggregierten Worst-Case-Werten über alle Dokumente. // Single aggregation row — CASE inside MAX() avoids the mixed-aggregate bug.
// CASE-Ausdrücke in MAX() vermeiden das GROUP-BY-Problem bei gemischten
// Aggregat- und Nicht-Aggregat-Spalten.
$stmt = $pdo->prepare( $stmt = $pdo->prepare(
"SELECT "SELECT
MAX(last_sync_timestamp) AS maxAdvLastSync, MAX(last_sync_timestamp) AS maxAdvLastSync,
@@ -48,6 +46,12 @@ class UpdateLastSyncFromDocuments implements BeforeSave
OR ai_sync_status IS NULL OR ai_sync_status = '' THEN 1 OR ai_sync_status IS NULL OR ai_sync_status = '' THEN 1
ELSE 0 ELSE 0
END) AS aiWorstLevel, END) AS aiWorstLevel,
-- ai_sync_status = unclean triggers graphParsingStatus
SUM(CASE WHEN ai_sync_status = 'unclean' THEN 1 ELSE 0 END) AS aiUncleanCount,
-- aiParsingStatus buckets
SUM(CASE WHEN ai_parsing_status = 'parsing' THEN 1 ELSE 0 END) AS parseParsingCount,
SUM(CASE WHEN ai_parsing_status = 'complete' THEN 1 ELSE 0 END) AS parseCompleteCount,
SUM(CASE WHEN ai_parsing_status = 'failed' THEN 1 ELSE 0 END) AS parseFailedCount,
COUNT(*) AS docCount COUNT(*) AS docCount
FROM c_dokumente FROM c_dokumente
WHERE c_akten_id = :aktenId AND deleted = 0" WHERE c_akten_id = :aktenId AND deleted = 0"
@@ -58,10 +62,13 @@ class UpdateLastSyncFromDocuments implements BeforeSave
if (!$row || (int)$row['docCount'] === 0) { if (!$row || (int)$row['docCount'] === 0) {
$entity->set('syncStatus', 'unclean'); $entity->set('syncStatus', 'unclean');
$entity->set('aiSyncStatus', 'unclean'); $entity->set('aiSyncStatus', 'unclean');
$entity->set('aiParsingStatus', 'unknown');
return; return;
} }
// Timestamps setzen $docCount = (int)$row['docCount'];
// ── Timestamps ─────────────────────────────────────────────────
if (!empty($row['maxAdvLastSync'])) { if (!empty($row['maxAdvLastSync'])) {
$entity->set('lastSync', $row['maxAdvLastSync']); $entity->set('lastSync', $row['maxAdvLastSync']);
} }
@@ -69,7 +76,7 @@ class UpdateLastSyncFromDocuments implements BeforeSave
$entity->set('aiLastSync', $row['maxAiLastSync']); $entity->set('aiLastSync', $row['maxAiLastSync']);
} }
// Advoware Status setzen (worst-case über alle Dokumente) // ── Advoware Sync Status (worst-case) ──────────────────────────
$advLevel = (int)($row['advWorstLevel'] ?? 0); $advLevel = (int)($row['advWorstLevel'] ?? 0);
if ($advLevel >= 2) { if ($advLevel >= 2) {
$entity->set('syncStatus', 'failed'); $entity->set('syncStatus', 'failed');
@@ -79,7 +86,7 @@ class UpdateLastSyncFromDocuments implements BeforeSave
$entity->set('syncStatus', 'synced'); $entity->set('syncStatus', 'synced');
} }
// AI Status setzen (worst-case über alle Dokumente) // ── AI Sync Status (worst-case) ────────────────────────────────
$aiLevel = (int)($row['aiWorstLevel'] ?? 0); $aiLevel = (int)($row['aiWorstLevel'] ?? 0);
if ($aiLevel >= 2) { if ($aiLevel >= 2) {
$entity->set('aiSyncStatus', 'failed'); $entity->set('aiSyncStatus', 'failed');
@@ -89,6 +96,31 @@ class UpdateLastSyncFromDocuments implements BeforeSave
$entity->set('aiSyncStatus', 'synced'); $entity->set('aiSyncStatus', 'synced');
} }
// ── AI Parsing Status (aggregated across all documents) ─────────
// Priority: parsing > unknown > complete_with_failures > complete
$parseParsing = (int)($row['parseParsingCount'] ?? 0);
$parseComplete = (int)($row['parseCompleteCount'] ?? 0);
$parseFailed = (int)($row['parseFailedCount'] ?? 0);
$parseUnknown = $docCount - $parseParsing - $parseComplete - $parseFailed;
if ($parseParsing > 0) {
$entity->set('aiParsingStatus', 'parsing');
} elseif ($parseUnknown > 0) {
$entity->set('aiParsingStatus', 'unknown');
} elseif ($parseFailed > 0) {
// No unknown/parsing left, but some failures
$entity->set('aiParsingStatus', 'complete_with_failures');
} else {
$entity->set('aiParsingStatus', 'complete');
}
// ── Graph Parsing Status (only auto-set to unclean, never reset) ─
// If any document's AI sync status is currently unclean → graph is stale.
// Any other transition (back to complete, etc.) must be done manually.
if ((int)($row['aiUncleanCount'] ?? 0) > 0) {
$entity->set('graphParsingStatus', 'unclean');
}
} catch (\Exception $e) { } catch (\Exception $e) {
$GLOBALS['log']->error('CAkten UpdateLastSyncFromDocuments Hook Error: ' . $e->getMessage()); $GLOBALS['log']->error('CAkten UpdateLastSyncFromDocuments Hook Error: ' . $e->getMessage());
} }

View File

@@ -68,6 +68,7 @@ class SyncStatusOnRelate implements AfterRelate, AfterUnrelate
$entity->set('cAktenId', $advowareAkten->getId()); $entity->set('cAktenId', $advowareAkten->getId());
$entity->set('syncStatus', 'unclean'); $entity->set('syncStatus', 'unclean');
$entity->set('aiSyncStatus', 'unclean'); $entity->set('aiSyncStatus', 'unclean');
$entity->set('aiParsingStatus', 'unknown');
$this->entityManager->saveEntity($entity, ['silent' => true, 'skipHooks' => true]); $this->entityManager->saveEntity($entity, ['silent' => true, 'skipHooks' => true]);
$this->triggerAkteUpdate($advowareAkten->getId()); $this->triggerAkteUpdate($advowareAkten->getId());
@@ -75,6 +76,7 @@ class SyncStatusOnRelate implements AfterRelate, AfterUnrelate
// Kein Akte-Link — trotzdem Sync-Status auf unclean setzen // Kein Akte-Link — trotzdem Sync-Status auf unclean setzen
$entity->set('syncStatus', 'unclean'); $entity->set('syncStatus', 'unclean');
$entity->set('aiSyncStatus', 'unclean'); $entity->set('aiSyncStatus', 'unclean');
$entity->set('aiParsingStatus', 'unknown');
$this->entityManager->saveEntity($entity, ['silent' => true, 'skipHooks' => true]); $this->entityManager->saveEntity($entity, ['silent' => true, 'skipHooks' => true]);
} }

View File

@@ -48,6 +48,7 @@ class UpdateJunctionSyncStatus implements AfterSave, AfterRemove
if ($newAktenId) { if ($newAktenId) {
$entity->set('syncStatus', 'unclean'); $entity->set('syncStatus', 'unclean');
$entity->set('aiSyncStatus', 'unclean'); $entity->set('aiSyncStatus', 'unclean');
$entity->set('aiParsingStatus', 'unknown');
$this->entityManager->saveEntity($entity, ['silent' => true, 'skipHooks' => true]); $this->entityManager->saveEntity($entity, ['silent' => true, 'skipHooks' => true]);
$this->triggerAkteUpdate($newAktenId); $this->triggerAkteUpdate($newAktenId);
} }
@@ -70,6 +71,7 @@ class UpdateJunctionSyncStatus implements AfterSave, AfterRemove
$entity->set('syncStatus', 'unclean'); $entity->set('syncStatus', 'unclean');
$entity->set('aiSyncStatus', 'unclean'); $entity->set('aiSyncStatus', 'unclean');
$entity->set('aiParsingStatus', 'unknown');
$this->entityManager->saveEntity($entity, ['silent' => true, 'skipHooks' => true]); $this->entityManager->saveEntity($entity, ['silent' => true, 'skipHooks' => true]);
$this->triggerAkteUpdate($entity->get('cAktenId')); $this->triggerAkteUpdate($entity->get('cAktenId'));

View File

@@ -27,7 +27,9 @@
"globalLastSync": "Globaler letzter Sync", "globalLastSync": "Globaler letzter Sync",
"syncSchalter": "Sync aktiv", "syncSchalter": "Sync aktiv",
"dokumentes": "Dokumente", "dokumentes": "Dokumente",
"rubrum": "Rubrum" "rubrum": "Rubrum",
"aiParsingStatus": "AI Parsing-Status",
"graphParsingStatus": "Graph Parsing-Status"
}, },
"options": { "options": {
"syncStatus": { "syncStatus": {
@@ -65,6 +67,18 @@
"unclean": "Nicht synchronisiert", "unclean": "Nicht synchronisiert",
"pending_sync": "Synchronisierung ausstehend", "pending_sync": "Synchronisierung ausstehend",
"failed": "Fehlgeschlagen" "failed": "Fehlgeschlagen"
},
"aiParsingStatus": {
"unknown": "Unbekannt",
"parsing": "In Verarbeitung",
"complete": "Abgeschlossen",
"complete_with_failures": "Abgeschlossen mit Fehlern"
},
"graphParsingStatus": {
"no_graph": "Kein Graph",
"parsing": "In Verarbeitung",
"complete": "Abgeschlossen",
"unclean": "Veraltet"
} }
}, },
"tooltips": { "tooltips": {

View File

@@ -30,7 +30,8 @@
"aiSyncHash": "AI Sync-Hash", "aiSyncHash": "AI Sync-Hash",
"aiSyncStatus": "AI Sync-Status", "aiSyncStatus": "AI Sync-Status",
"aiLastSync": "AI Letzter Sync", "aiLastSync": "AI Letzter Sync",
"aiFileId": "AI File-ID" "aiFileId": "AI File-ID",
"aiParsingStatus": "AI Parsing-Status"
}, },
"links": { "links": {
"contactsvmhdokumente": "Freigegebene Nutzer", "contactsvmhdokumente": "Freigegebene Nutzer",
@@ -77,6 +78,12 @@
"synced": "Synchronisiert", "synced": "Synchronisiert",
"failed": "Fehler", "failed": "Fehler",
"unsupported": "Nicht unterstützt" "unsupported": "Nicht unterstützt"
},
"aiParsingStatus": {
"unknown": "Unbekannt",
"parsing": "In Verarbeitung",
"complete": "Abgeschlossen",
"failed": "Fehlgeschlagen"
} }
} }
} }

View File

@@ -28,7 +28,9 @@
"globalLastSync": "Global Last Sync", "globalLastSync": "Global Last Sync",
"syncSchalter": "Sync Active", "syncSchalter": "Sync Active",
"dokumentes": "Documents", "dokumentes": "Documents",
"rubrum": "Rubrum" "rubrum": "Rubrum",
"aiParsingStatus": "AI Parsing Status",
"graphParsingStatus": "Graph Parsing Status"
}, },
"options": { "options": {
"syncStatus": { "syncStatus": {
@@ -66,6 +68,18 @@
"unclean": "Not Synchronized", "unclean": "Not Synchronized",
"pending_sync": "Synchronization Pending", "pending_sync": "Synchronization Pending",
"failed": "Failed" "failed": "Failed"
},
"aiParsingStatus": {
"unknown": "Unknown",
"parsing": "Parsing",
"complete": "Complete",
"complete_with_failures": "Complete with Failures"
},
"graphParsingStatus": {
"no_graph": "No Graph",
"parsing": "Parsing",
"complete": "Complete",
"unclean": "Stale"
} }
}, },
"tooltips": { "tooltips": {

View File

@@ -30,7 +30,8 @@
"aiSyncHash": "AI Sync Hash", "aiSyncHash": "AI Sync Hash",
"aiSyncStatus": "AI Sync Status", "aiSyncStatus": "AI Sync Status",
"aiLastSync": "AI Last Sync", "aiLastSync": "AI Last Sync",
"aiFileId": "AI File ID" "aiFileId": "AI File ID",
"aiParsingStatus": "AI Parsing Status"
}, },
"links": { "links": {
"contactsvmhdokumente": "Portal Users", "contactsvmhdokumente": "Portal Users",
@@ -83,6 +84,12 @@
"synced": "Synchronized", "synced": "Synchronized",
"failed": "Failed", "failed": "Failed",
"unsupported": "Unsupported" "unsupported": "Unsupported"
},
"aiParsingStatus": {
"unknown": "Unknown",
"parsing": "Parsing",
"complete": "Complete",
"failed": "Failed"
} }
} }
} }

View File

@@ -74,6 +74,14 @@
"name": "aiProvider" "name": "aiProvider"
}, },
{} {}
],
[
{
"name": "aiParsingStatus"
},
{
"name": "graphParsingStatus"
}
] ]
], ],
"style": "default", "style": "default",

View File

@@ -122,7 +122,9 @@
{ {
"name": "aiSyncHash" "name": "aiSyncHash"
}, },
{"isFull": false} {
"name": "aiParsingStatus"
}
] ]
], ],
"dynamicLogicVisible": null, "dynamicLogicVisible": null,

View File

@@ -19,6 +19,10 @@
"name": "aiSyncStatus", "name": "aiSyncStatus",
"align": "left" "align": "left"
}, },
{
"name": "aiParsingStatus",
"align": "left"
},
{ {
"name": "aiLastSync", "name": "aiLastSync",
"align": "left" "align": "left"

View File

@@ -216,6 +216,45 @@
"maxLength": 500, "maxLength": 500,
"tooltip": true, "tooltip": true,
"isCustom": true "isCustom": true
},
"aiParsingStatus": {
"type": "enum",
"required": false,
"options": [
"unknown",
"parsing",
"complete",
"complete_with_failures"
],
"style": {
"unknown": "default",
"parsing": "info",
"complete": "success",
"complete_with_failures": "warning"
},
"default": "unknown",
"readOnly": true,
"tooltip": true,
"isCustom": true
},
"graphParsingStatus": {
"type": "enum",
"required": false,
"options": [
"no_graph",
"parsing",
"complete",
"unclean"
],
"style": {
"no_graph": "default",
"parsing": "info",
"complete": "success",
"unclean": "warning"
},
"default": "no_graph",
"tooltip": true,
"isCustom": true
} }
}, },
"links": { "links": {

View File

@@ -213,6 +213,24 @@
"maxLength": 255, "maxLength": 255,
"tooltip": true, "tooltip": true,
"isCustom": true "isCustom": true
},
"aiParsingStatus": {
"type": "enum",
"options": [
"unknown",
"parsing",
"complete",
"failed"
],
"style": {
"unknown": "default",
"parsing": "info",
"complete": "success",
"failed": "danger"
},
"default": "unknown",
"tooltip": true,
"isCustom": true
} }
}, },
"links": { "links": {

View File

@@ -359,7 +359,7 @@ return [
0 => 'youtube.com', 0 => 'youtube.com',
1 => 'google.com' 1 => 'google.com'
], ],
'microtime' => 1774603769.791242, 'microtime' => 1774604561.068253,
'siteUrl' => 'https://crm.bitbylaw.com', 'siteUrl' => 'https://crm.bitbylaw.com',
'fullTextSearchMinLength' => 4, 'fullTextSearchMinLength' => 4,
'webSocketUrl' => 'ws://api.bitbylaw.com:5000/espocrm/ws', 'webSocketUrl' => 'ws://api.bitbylaw.com:5000/espocrm/ws',

View File

@@ -1,7 +1,7 @@
<?php <?php
return [ return [
'cacheTimestamp' => 1774603769, 'cacheTimestamp' => 1774604771,
'microtimeState' => 1774603769.963245, 'microtimeState' => 1774604771.465835,
'currencyRates' => [ 'currencyRates' => [
'EUR' => 1.0 'EUR' => 1.0
], ],