Zammad sync performance + liitteiden näyttö tikettilistassa
- Rajoita artikkelien haku max 10 tikettiin per sync (loput on-demand) - Curl timeout 15s + connect timeout 5s - Frontend: IMAP ja Zammad haetaan rinnakkain (Promise.allSettled) - Auto-refresh: Zammad sync ei blokkaa tikettien latausta - Hakaneula-ikoni (📎) tikettilistassa kun viestissä on liitteitä - has_attachments, source, zammad_group, ticket_number tikettilistaan Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
30
api.php
30
api.php
@@ -250,12 +250,13 @@ class ZammadClient {
|
|||||||
$this->token = $token;
|
$this->token = $token;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function request(string $method, string $endpoint, ?array $data = null): array {
|
private function request(string $method, string $endpoint, ?array $data = null, int $timeout = 15): array {
|
||||||
$url = $this->url . '/api/v1/' . ltrim($endpoint, '/');
|
$url = $this->url . '/api/v1/' . ltrim($endpoint, '/');
|
||||||
$ch = curl_init($url);
|
$ch = curl_init($url);
|
||||||
curl_setopt_array($ch, [
|
curl_setopt_array($ch, [
|
||||||
CURLOPT_RETURNTRANSFER => true,
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
CURLOPT_TIMEOUT => 30,
|
CURLOPT_TIMEOUT => $timeout,
|
||||||
|
CURLOPT_CONNECTTIMEOUT => 5,
|
||||||
CURLOPT_HTTPHEADER => [
|
CURLOPT_HTTPHEADER => [
|
||||||
'Authorization: Token token=' . $this->token,
|
'Authorization: Token token=' . $this->token,
|
||||||
'Content-Type: application/json',
|
'Content-Type: application/json',
|
||||||
@@ -3162,6 +3163,10 @@ switch ($action) {
|
|||||||
foreach ($tickets as $t) {
|
foreach ($tickets as $t) {
|
||||||
$msgCount = count($t['messages'] ?? []);
|
$msgCount = count($t['messages'] ?? []);
|
||||||
$lastMsg = $msgCount > 0 ? $t['messages'][$msgCount - 1] : null;
|
$lastMsg = $msgCount > 0 ? $t['messages'][$msgCount - 1] : null;
|
||||||
|
$hasAttachments = false;
|
||||||
|
foreach ($t['messages'] ?? [] as $m) {
|
||||||
|
if (!empty($m['attachments'])) { $hasAttachments = true; break; }
|
||||||
|
}
|
||||||
$list[] = [
|
$list[] = [
|
||||||
'id' => $t['id'],
|
'id' => $t['id'],
|
||||||
'subject' => $t['subject'],
|
'subject' => $t['subject'],
|
||||||
@@ -3184,6 +3189,10 @@ switch ($action) {
|
|||||||
'message_count' => $msgCount,
|
'message_count' => $msgCount,
|
||||||
'last_message_type' => $lastMsg ? ($lastMsg['type'] ?? '') : '',
|
'last_message_type' => $lastMsg ? ($lastMsg['type'] ?? '') : '',
|
||||||
'last_message_time' => $lastMsg ? ($lastMsg['timestamp'] ?? '') : '',
|
'last_message_time' => $lastMsg ? ($lastMsg['timestamp'] ?? '') : '',
|
||||||
|
'has_attachments' => $hasAttachments,
|
||||||
|
'source' => $t['source'] ?? '',
|
||||||
|
'zammad_group' => $t['zammad_group'] ?? '',
|
||||||
|
'ticket_number' => $t['ticket_number'] ?? '',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5366,6 +5375,8 @@ switch ($action) {
|
|||||||
$created = 0;
|
$created = 0;
|
||||||
$updated = 0;
|
$updated = 0;
|
||||||
$messagesAdded = 0;
|
$messagesAdded = 0;
|
||||||
|
$articlesFetched = 0;
|
||||||
|
$maxArticleFetches = 10; // Max tikettejä joille haetaan artikkelit per synk (loput on-demand)
|
||||||
|
|
||||||
// Zammad state → intran status
|
// Zammad state → intran status
|
||||||
$stateMap = [
|
$stateMap = [
|
||||||
@@ -5378,6 +5389,8 @@ switch ($action) {
|
|||||||
'1 low' => 'matala', '2 normal' => 'normaali', '3 high' => 'korkea',
|
'1 low' => 'matala', '2 normal' => 'normaali', '3 high' => 'korkea',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Vaihe 1: Synkkaa kaikki tiketit (nopea — ei artikkeleja)
|
||||||
|
$newTicketIds = []; // ticketId => zammadId — artikkelit haetaan toisessa vaiheessa
|
||||||
foreach ($allTickets as $zt) {
|
foreach ($allTickets as $zt) {
|
||||||
$zammadId = (int)$zt['id'];
|
$zammadId = (int)$zt['id'];
|
||||||
$existing = dbGetTicketByZammadId($companyId, $zammadId);
|
$existing = dbGetTicketByZammadId($companyId, $zammadId);
|
||||||
@@ -5407,6 +5420,7 @@ switch ($action) {
|
|||||||
$zt['updated_at'] ? date('Y-m-d H:i:s', strtotime($zt['updated_at'])) : $now]
|
$zt['updated_at'] ? date('Y-m-d H:i:s', strtotime($zt['updated_at'])) : $now]
|
||||||
);
|
);
|
||||||
$created++;
|
$created++;
|
||||||
|
$newTicketIds[$ticketId] = $zammadId;
|
||||||
} else {
|
} else {
|
||||||
$ticketId = $existing['id'];
|
$ticketId = $existing['id'];
|
||||||
$zammadUpdated = date('Y-m-d H:i:s', strtotime($zt['updated_at'] ?? 'now'));
|
$zammadUpdated = date('Y-m-d H:i:s', strtotime($zt['updated_at'] ?? 'now'));
|
||||||
@@ -5417,14 +5431,15 @@ switch ($action) {
|
|||||||
);
|
);
|
||||||
$updated++;
|
$updated++;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Synkkaa artikkelit vain uusille tiketeille (ei jokaiselle — liian hidas)
|
// Vaihe 2: Hae artikkelit max N uusimmalle uudelle tiketille (loput on-demand)
|
||||||
// Olemassa olevien tikettien artikkelit haetaan on-demand kun käyttäjä avaa tiketin
|
$articleQueue = array_slice($newTicketIds, 0, $maxArticleFetches, true);
|
||||||
if ($existing) continue;
|
foreach ($articleQueue as $ticketId => $zammadId) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$articles = $z->getArticles($zammadId);
|
$articles = $z->getArticles($zammadId);
|
||||||
$toEmail = '';
|
$toEmail = '';
|
||||||
|
$articlesFetched++;
|
||||||
foreach ($articles as $art) {
|
foreach ($articles as $art) {
|
||||||
if (($art['internal'] ?? false)) continue;
|
if (($art['internal'] ?? false)) continue;
|
||||||
// Poimi zammad_to_email ensimmäisestä asiakkaan viestistä
|
// Poimi zammad_to_email ensimmäisestä asiakkaan viestistä
|
||||||
@@ -5480,12 +5495,15 @@ switch ($action) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$skippedArticles = max(0, count($newTicketIds) - $maxArticleFetches);
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
'ok' => true,
|
'ok' => true,
|
||||||
'tickets_found' => count($allTickets),
|
'tickets_found' => count($allTickets),
|
||||||
'created' => $created,
|
'created' => $created,
|
||||||
'updated' => $updated,
|
'updated' => $updated,
|
||||||
'messages_added' => $messagesAdded,
|
'messages_added' => $messagesAdded,
|
||||||
|
'articles_fetched' => $articlesFetched,
|
||||||
|
'articles_deferred' => $skippedArticles, // Haetaan on-demand kun tiketti avataan
|
||||||
]);
|
]);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
http_response_code(500);
|
http_response_code(500);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Noxus HUB</title>
|
<title>Noxus HUB</title>
|
||||||
<link rel="stylesheet" href="style.css?v=20260313r">
|
<link rel="stylesheet" href="style.css?v=20260313s">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- Login -->
|
<!-- Login -->
|
||||||
@@ -2271,6 +2271,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="script.js?v=20260313r"></script>
|
<script src="script.js?v=20260313s"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
39
script.js
39
script.js
@@ -1491,7 +1491,7 @@ function renderTickets() {
|
|||||||
<td><span class="ticket-status ticket-status-${t.status}">${ticketStatusLabels[t.status] || t.status}</span></td>
|
<td><span class="ticket-status ticket-status-${t.status}">${ticketStatusLabels[t.status] || t.status}</span></td>
|
||||||
<td>${t.customer_name ? esc(t.customer_name) : '<span style="color:#ccc;">-</span>'}</td>
|
<td>${t.customer_name ? esc(t.customer_name) : '<span style="color:#ccc;">-</span>'}</td>
|
||||||
<td><span class="ticket-type ticket-type-${t.type || 'muu'}">${typeLabel}</span></td>
|
<td><span class="ticket-type ticket-type-${t.type || 'muu'}">${typeLabel}</span></td>
|
||||||
<td>${prioBadge}${companyBadge}${t.ticket_number ? `<span style="color:#888;font-size:0.8rem;margin-right:0.3rem;">#${t.ticket_number}</span>` : ''}<strong>${esc(t.subject)}</strong></td>
|
<td>${prioBadge}${companyBadge}${t.ticket_number ? `<span style="color:#888;font-size:0.8rem;margin-right:0.3rem;">#${t.ticket_number}</span>` : ''}${t.has_attachments ? '<span title="Liitteitä" style="color:#888;margin-right:0.3rem;">📎</span>' : ''}<strong>${esc(t.subject)}</strong></td>
|
||||||
<td>${esc(t.mailbox_name || t.from_name || t.from_email)}</td>
|
<td>${esc(t.mailbox_name || t.from_name || t.from_email)}</td>
|
||||||
<td style="text-align:center;">${lastType} ${t.message_count}</td>
|
<td style="text-align:center;">${lastType} ${t.message_count}</td>
|
||||||
<td class="nowrap" title="${esc((t.updated || '').substring(0, 16))}">${timeAgo(t.updated)}</td>
|
<td class="nowrap" title="${esc((t.updated || '').substring(0, 16))}">${timeAgo(t.updated)}</td>
|
||||||
@@ -2145,22 +2145,33 @@ document.getElementById('btn-fetch-emails').addEventListener('click', async () =
|
|||||||
status.textContent = 'Yhdistetään sähköpostipalvelimeen...';
|
status.textContent = 'Yhdistetään sähköpostipalvelimeen...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await apiCall('ticket_fetch', 'POST');
|
// Hae IMAP ja Zammad rinnakkain — ei enää peräkkäin
|
||||||
let statusMsg = `Valmis! ${result.new_tickets} uutta tikettiä, ${result.threaded} ketjutettu viestiä.`;
|
status.textContent = 'Haetaan sähköpostit ja synkataan Zammad...';
|
||||||
|
const [imapResult, zammadResult] = await Promise.allSettled([
|
||||||
|
apiCall('ticket_fetch', 'POST'),
|
||||||
|
apiCall('zammad_sync', 'POST', { full: true }),
|
||||||
|
]);
|
||||||
|
|
||||||
// Hae myös Zammadista (full sync)
|
let parts = [];
|
||||||
let zammadMsg = '';
|
if (imapResult.status === 'fulfilled') {
|
||||||
try {
|
const r = imapResult.value;
|
||||||
status.textContent = 'Synkataan Zammad...';
|
parts.push(`📧 ${r.new_tickets} uutta, ${r.threaded} ketjutettu`);
|
||||||
const zResult = await apiCall('zammad_sync', 'POST', { full: true });
|
} else if (imapResult.reason) {
|
||||||
if (zResult.created || zResult.updated || zResult.messages_added) {
|
parts.push(`📧 Virhe: ${imapResult.reason.message || 'tuntematon'}`);
|
||||||
zammadMsg = ` Zammad: ${zResult.created} uutta, ${zResult.updated} päivitettyä, ${zResult.messages_added} viestiä.`;
|
}
|
||||||
|
if (zammadResult.status === 'fulfilled' && zammadResult.value.ok) {
|
||||||
|
const z = zammadResult.value;
|
||||||
|
const zParts = [];
|
||||||
|
if (z.created) zParts.push(`${z.created} uutta`);
|
||||||
|
if (z.updated) zParts.push(`${z.updated} päivitettyä`);
|
||||||
|
if (z.messages_added) zParts.push(`${z.messages_added} viestiä`);
|
||||||
|
if (z.articles_deferred) zParts.push(`${z.articles_deferred} tiketin viestit haetaan kun avaat`);
|
||||||
|
if (zParts.length) parts.push(`🔗 Zammad: ${zParts.join(', ')}`);
|
||||||
}
|
}
|
||||||
} catch (ze) { /* Zammad ei käytössä tai virhe — ohitetaan */ }
|
|
||||||
|
|
||||||
status.style.background = '#eafaf1';
|
status.style.background = '#eafaf1';
|
||||||
status.style.color = '#27ae60';
|
status.style.color = '#27ae60';
|
||||||
status.textContent = statusMsg + zammadMsg;
|
status.textContent = parts.length ? parts.join(' | ') : 'Valmis — ei uusia viestejä.';
|
||||||
await loadTickets();
|
await loadTickets();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
status.style.background = '#fef2f2';
|
status.style.background = '#fef2f2';
|
||||||
@@ -2185,8 +2196,8 @@ function startTicketAutoRefresh() {
|
|||||||
const supportActive = document.getElementById('tab-content-support').classList.contains('active');
|
const supportActive = document.getElementById('tab-content-support').classList.contains('active');
|
||||||
const listVisible = document.getElementById('ticket-list-view').style.display !== 'none';
|
const listVisible = document.getElementById('ticket-list-view').style.display !== 'none';
|
||||||
if (supportActive && listVisible) {
|
if (supportActive && listVisible) {
|
||||||
// Synkkaa Zammad taustalla ennen tikettien latausta
|
// Synkkaa Zammad taustalla JA lataa tiketit rinnakkain — ei blokkaa
|
||||||
try { await apiCall('zammad_sync', 'POST'); } catch (e) { /* Zammad ei käytössä */ }
|
apiCall('zammad_sync', 'POST').catch(() => {});
|
||||||
loadTickets();
|
loadTickets();
|
||||||
}
|
}
|
||||||
}, seconds * 1000);
|
}, seconds * 1000);
|
||||||
|
|||||||
Reference in New Issue
Block a user