Skip to content

Commit d9cb2c1

Browse files
Merge pull request #1475 from liberu-genealogy/copilot/investigate-gedcom-upload-issue
Fix GEDCOM upload job dispatch and add live import progress tracking
2 parents 9fca836 + d95a4b0 commit d9cb2c1

File tree

10 files changed

+350
-51
lines changed

10 files changed

+350
-51
lines changed

app/Filament/App/Resources/GedcomResource/Pages/CreateGedcom.php

Lines changed: 48 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,35 @@
33
namespace App\Filament\App\Resources\GedcomResource\Pages;
44

55
use App\Filament\App\Resources\GedcomResource;
6+
use App\Filament\App\Resources\ImportJobResource;
67
use App\Jobs\ImportGedcom;
78
use App\Jobs\ImportGrampsXml;
9+
use App\Models\ImportJob;
810
use Filament\Notifications\Notification;
911
use Filament\Resources\Pages\CreateRecord;
1012
use Illuminate\Support\Facades\Auth;
1113
use Illuminate\Support\Facades\Log;
1214
use Illuminate\Support\Facades\Storage;
15+
use Illuminate\Support\Str;
1316
use Throwable;
1417

1518
class CreateGedcom extends CreateRecord
1619
{
1720
protected static string $resource = GedcomResource::class;
1821

22+
/** The import slug created before dispatch, used for the redirect. */
23+
private string $importSlug = '';
24+
1925
protected function afterCreate(): void
2026
{
2127
$record = $this->getRecord();
22-
$path = $record->filename;
28+
$path = $record->filename;
29+
30+
// FileUpload may store as array (e.g. when Filament uses array persistence)
31+
if (is_array($path)) {
32+
$filtered = array_values(array_filter($path));
33+
$path = $filtered !== [] ? $filtered[0] : null;
34+
}
2335

2436
Log::info('CreateGedcom::afterCreate called', [
2537
'gedcom_id' => $record->getKey(),
@@ -34,34 +46,56 @@ protected function afterCreate(): void
3446
return;
3547
}
3648

37-
$fullPath = Storage::disk('private')->path($path);
49+
$fullPath = Storage::disk('private')->path($path);
3850
$extension = strtolower(pathinfo($fullPath, PATHINFO_EXTENSION));
3951

52+
// Pre-create the ImportJob so the user can track it immediately
53+
$slug = (string) Str::uuid();
54+
ImportJob::create([
55+
'user_id' => Auth::id(),
56+
'status' => 'queue',
57+
'slug' => $slug,
58+
'progress' => 0,
59+
]);
60+
$this->importSlug = $slug;
61+
4062
try {
41-
// Dispatch appropriate import job based on file extension
63+
// Dispatch the appropriate import job, passing the pre-created slug
4264
if (in_array($extension, ['gramps', 'xml'])) {
43-
ImportGrampsXml::dispatch(Auth::user(), $fullPath);
44-
Log::info('Dispatched GrampsXML import', ['path' => $path, 'full_path' => $fullPath]);
65+
ImportGrampsXml::dispatch(Auth::user(), $fullPath, $slug);
66+
Log::info('Dispatched GrampsXML import', ['path' => $path, 'full_path' => $fullPath, 'slug' => $slug]);
4567
} else {
46-
ImportGedcom::dispatch(Auth::user(), $fullPath);
47-
Log::info('Dispatched GEDCOM import', ['path' => $path, 'full_path' => $fullPath]);
68+
ImportGedcom::dispatch(Auth::user(), $fullPath, $slug);
69+
Log::info('Dispatched GEDCOM import', ['path' => $path, 'full_path' => $fullPath, 'slug' => $slug]);
4870
}
4971

5072
Notification::make()
5173
->title('GEDCOM import queued')
52-
->body('Your file is being processed. Check Import Logs to monitor progress.')
74+
->body('Your file is being processed. The Import Logs page below shows live progress.')
5375
->success()
5476
->send();
5577
} catch (Throwable $e) {
5678
Log::error('Failed to dispatch GEDCOM import job', [
57-
'gedcom_id' => $record->getKey(),
58-
'path' => $path,
59-
'full_path' => $fullPath,
60-
'error' => $e->getMessage(),
61-
'trace' => $e->getTraceAsString(),
79+
'gedcom_id' => $record->getKey(),
80+
'path' => $path,
81+
'full_path' => $fullPath,
82+
'error' => $e->getMessage(),
83+
'trace' => $e->getTraceAsString(),
6284
]);
6385

64-
throw $e;
86+
Notification::make()
87+
->title('Import failed')
88+
->body($e->getMessage())
89+
->danger()
90+
->send();
6591
}
6692
}
93+
94+
/**
95+
* After creation, redirect to Import Logs so the user can watch progress.
96+
*/
97+
protected function getRedirectUrl(): string
98+
{
99+
return ImportJobResource::getUrl('index');
100+
}
67101
}

app/Filament/App/Resources/ImportJobResource.php

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
namespace App\Filament\App\Resources;
44

55
use App\Filament\App\Resources\ImportJobResource\Pages\ListImportJobs;
6+
use App\Filament\App\Resources\ImportJobResource\Pages\ViewImportJob;
67
use App\Models\ImportJob;
8+
use Filament\Tables\Actions\ViewAction;
79
use Filament\Tables\Columns\TextColumn;
810
use Filament\Tables\Table;
911
use Illuminate\Database\Eloquent\Builder;
@@ -41,35 +43,66 @@ public static function table(Table $table): Table
4143
TextColumn::make('slug')
4244
->label('Import ID')
4345
->searchable()
44-
->copyable(),
46+
->copyable()
47+
->limit(16),
4548
TextColumn::make('status')
4649
->label('Status')
4750
->badge()
4851
->color(fn (string $state): string => match ($state) {
49-
'complete' => 'success',
50-
'failed' => 'danger',
51-
'queue' => 'warning',
52-
default => 'gray',
52+
'complete' => 'success',
53+
'failed' => 'danger',
54+
'processing' => 'info',
55+
'queue' => 'warning',
56+
default => 'gray',
5357
}),
58+
TextColumn::make('progress')
59+
->label('Progress')
60+
->formatStateUsing(fn (int $state): string => $state . '%')
61+
->color(fn (int $state): string => match (true) {
62+
$state === 100 => 'success',
63+
$state >= 50 => 'info',
64+
$state > 0 => 'warning',
65+
default => 'gray',
66+
}),
67+
TextColumn::make('people_imported')
68+
->label('People')
69+
->numeric()
70+
->default(0)
71+
->toggleable(),
72+
TextColumn::make('families_imported')
73+
->label('Families')
74+
->numeric()
75+
->default(0)
76+
->toggleable(),
77+
TextColumn::make('error_message')
78+
->label('Error')
79+
->limit(60)
80+
->tooltip(fn (?string $state): string => $state ?? '')
81+
->color('danger')
82+
->toggleable(),
5483
TextColumn::make('created_at')
55-
->label('Started At')
84+
->label('Queued At')
5685
->dateTime()
5786
->sortable(),
5887
TextColumn::make('updated_at')
5988
->label('Last Updated')
6089
->dateTime()
61-
->sortable(),
90+
->sortable()
91+
->since(),
6292
])
6393
->defaultSort('created_at', 'desc')
6494
->filters([])
65-
->recordActions([])
95+
->recordActions([
96+
ViewAction::make(),
97+
])
6698
->toolbarActions([]);
6799
}
68100

69101
public static function getPages(): array
70102
{
71103
return [
72104
'index' => ListImportJobs::route('/'),
105+
'view' => ViewImportJob::route('/{record}'),
73106
];
74107
}
75108
}

app/Filament/App/Resources/ImportJobResource/Pages/ListImportJobs.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ class ListImportJobs extends ListRecords
99
{
1010
protected static string $resource = ImportJobResource::class;
1111

12+
/** Auto-refresh every 3 seconds so in-progress imports update live. */
13+
protected static ?string $pollingInterval = '3s';
14+
1215
protected function getHeaderActions(): array
1316
{
1417
return [];
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<?php
2+
3+
namespace App\Filament\App\Resources\ImportJobResource\Pages;
4+
5+
use App\Filament\App\Resources\ImportJobResource;
6+
use Filament\Forms\Components\Grid;
7+
use Filament\Forms\Components\Section;
8+
use Filament\Infolists\Components\TextEntry;
9+
use Filament\Resources\Pages\ViewRecord;
10+
use Filament\Schemas\Schema;
11+
12+
class ViewImportJob extends ViewRecord
13+
{
14+
protected static string $resource = ImportJobResource::class;
15+
16+
/** Auto-refresh every 3 seconds while import is in progress. */
17+
protected static ?string $pollingInterval = '3s';
18+
19+
protected function getHeaderActions(): array
20+
{
21+
return [];
22+
}
23+
24+
public function infolist(Schema $schema): Schema
25+
{
26+
return $schema
27+
->components([
28+
Section::make('Import Status')
29+
->schema([
30+
Grid::make(3)
31+
->schema([
32+
TextEntry::make('slug')
33+
->label('Import ID')
34+
->copyable(),
35+
TextEntry::make('status')
36+
->label('Status')
37+
->badge()
38+
->color(fn (string $state): string => match ($state) {
39+
'complete' => 'success',
40+
'failed' => 'danger',
41+
'processing' => 'info',
42+
'queue' => 'warning',
43+
default => 'gray',
44+
}),
45+
TextEntry::make('progress')
46+
->label('Progress')
47+
->formatStateUsing(fn (int $state): string => $state . '%')
48+
->color(fn (int $state): string => match (true) {
49+
$state === 100 => 'success',
50+
$state >= 50 => 'info',
51+
$state > 0 => 'warning',
52+
default => 'gray',
53+
}),
54+
]),
55+
Grid::make(2)
56+
->schema([
57+
TextEntry::make('people_imported')
58+
->label('People Imported')
59+
->numeric(),
60+
TextEntry::make('families_imported')
61+
->label('Families Imported')
62+
->numeric(),
63+
]),
64+
]),
65+
66+
Section::make('Error Details')
67+
->schema([
68+
TextEntry::make('error_message')
69+
->label('Error Message')
70+
->columnSpanFull()
71+
->color('danger')
72+
->placeholder('No errors'),
73+
])
74+
->collapsible(),
75+
76+
Section::make('Timestamps')
77+
->schema([
78+
Grid::make(2)
79+
->schema([
80+
TextEntry::make('created_at')
81+
->label('Queued At')
82+
->dateTime(),
83+
TextEntry::make('updated_at')
84+
->label('Last Updated')
85+
->dateTime()
86+
->since(),
87+
]),
88+
])
89+
->collapsible(),
90+
]);
91+
}
92+
}

app/Jobs/ImportGedcom.php

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ class ImportGedcom implements ShouldQueue
2727
public int $timeout = 0;
2828
public int $tries = 1;
2929

30-
public function __construct(protected User $user, protected string $filePath, protected ?string $slug = null)
30+
public function __construct(protected User $user, protected string $filePath, public ?string $slug = null)
3131
{
3232
}
3333

@@ -38,22 +38,37 @@ public function handle(): int
3838
'user_id' => $this->user->getKey(),
3939
]);
4040

41-
throw_unless(File::isFile($this->filePath), Exception::class, "{$this->filePath} does not exist.");
41+
// Find or create the ImportJob record
42+
$slug = $this->slug ?? (string) Str::uuid();
43+
$importJob = ImportJob::firstOrCreate(
44+
['slug' => $slug],
45+
[
46+
'user_id' => $this->user->getKey(),
47+
'status' => 'queue',
48+
'progress' => 0,
49+
],
50+
);
4251

43-
$slug = $this->slug ?? Str::uuid();
44-
45-
$job = ImportJob::create([
46-
'user_id' => $this->user->getKey(),
47-
'status' => 'queue',
48-
'slug' => $slug,
49-
]);
52+
$importJob->update(['status' => 'processing', 'progress' => 10]);
5053

5154
try {
55+
throw_unless(File::isFile($this->filePath), Exception::class, "{$this->filePath} does not exist.");
56+
57+
$importJob->update(['progress' => 25]);
58+
5259
$parser = new GedcomParser();
5360
$team_id = $this->user->currentTeam?->id;
61+
62+
$importJob->update(['status' => 'processing', 'progress' => 50]);
63+
5464
$parser->parse(config('database.default'), $this->filePath, $slug, true, $team_id);
65+
66+
$importJob->update(['progress' => 90]);
5567
} catch (Throwable $e) {
56-
$job->update(['status' => 'failed']);
68+
$importJob->update([
69+
'status' => 'failed',
70+
'error_message' => $e->getMessage(),
71+
]);
5772
Log::error('ImportGedcom parser failed', [
5873
'file_path' => $this->filePath,
5974
'user_id' => $this->user->getKey(),
@@ -63,7 +78,7 @@ public function handle(): int
6378
throw $e;
6479
}
6580

66-
$job->update(['status' => 'complete']);
81+
$importJob->update(['status' => 'complete', 'progress' => 100]);
6782

6883
Log::info('ImportGedcom job completed', [
6984
'file_path' => $this->filePath,

0 commit comments

Comments
 (0)