freescout2ant/Console/Commands/ListFailedArchives.php
Claude 3c3a092f7a
Track failed Ameise archive attempts and add retry tooling
Previously a thread could be skipped silently in ConversationArchiver
(no customer match, ambiguous match, API/network error, attachment
failure) without any record. Default-off logging hid the cause, and the
queue job swallowed exceptions, so messages "archived" in FreeScout
never surfaced as missing in Ameise.

Now every archive attempt records a row in the new crm_archive_attempts
table with status, sanitized HTTP response, attempt counter and
resolution timestamp. ArchiveThreadsJob retries transient failures up to
5 times with exponential backoff; ameise:list-failed-archives and
ameise:retry-failed-archives expose the queue from the CLI; the Ameise
settings page lists open failures with retry/dismiss buttons.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VU73s4KM5JZqVWQYPpK3cP
2026-06-16 23:06:49 +00:00

62 lines
2.1 KiB
PHP

<?php
namespace Modules\AmeiseModule\Console\Commands;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Modules\AmeiseModule\Entities\CrmArchiveAttempt;
class ListFailedArchives extends Command
{
protected $signature = 'ameise:list-failed-archives {--since=30d : Zeitraum, z. B. 24h, 7d, 30d} {--status= : Filter auf einen konkreten Status}';
protected $description = 'Listet unaufgeloeste Archivierungs-Fehlversuche aus crm_archive_attempts';
public function handle()
{
$since = $this->parseSince((string) $this->option('since'));
$status = $this->option('status');
$query = CrmArchiveAttempt::whereNull('resolved_at')
->whereIn('status', CrmArchiveAttempt::FAILURE_STATUSES)
->where('created_at', '>=', $since);
if ($status) {
$query->where('status', $status);
}
$attempts = $query->orderBy('created_at', 'desc')->get();
if ($attempts->isEmpty()) {
$this->info('Keine offenen Fehlversuche im Zeitraum.');
return 0;
}
$rows = $attempts->map(function ($a) {
return [
'id' => $a->id,
'conv' => $a->conversation_id,
'thread' => $a->thread_id,
'user' => $a->user_id,
'status' => $a->status,
'try' => $a->attempt_no,
'reason' => mb_strimwidth((string) $a->reason, 0, 60, '...'),
'when' => $a->created_at ? $a->created_at->toDateTimeString() : '',
];
})->all();
$this->table(['ID', 'Conv', 'Thread', 'User', 'Status', 'Try', 'Reason', 'Created'], $rows);
$this->info(sprintf('%d offene Fehlversuche.', $attempts->count()));
return 0;
}
private function parseSince(string $since): Carbon
{
if (preg_match('/^(\d+)\s*([hd])$/i', trim($since), $m)) {
$n = (int) $m[1];
return strtolower($m[2]) === 'h' ? Carbon::now()->subHours($n) : Carbon::now()->subDays($n);
}
return Carbon::now()->subDays(30);
}
}