Skip to content

Commit 70805fa

Browse files
authored
Merge pull request #89 from codebar-ag/feature-updates
Feature updates
2 parents e5ff8ea + ba2a061 commit 70805fa

11 files changed

Lines changed: 85 additions & 13 deletions

File tree

app/Actions/StoreLlmUsageAction.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,7 @@
77
use App\Models\AiModel;
88
use App\Models\AiModelDailyUsage;
99
use Illuminate\Support\Collection;
10-
use Illuminate\Support\Facades\Artisan;
1110
use Illuminate\Support\Facades\Cache;
12-
use Spatie\ResponseCache\Commands\ClearCommand;
1311

1412
class StoreLlmUsageAction
1513
{
@@ -35,10 +33,12 @@ public function store(Collection $rows): int
3533
update: ['ai_model_id', 'prompt_tokens', 'completion_tokens', 'total_tokens', 'requests', 'spend'],
3634
);
3735

36+
// Bumping the version invalidates every cached stats query at once, which is
37+
// all this action is responsible for. Clearing the rendered pages is the
38+
// sync's job, not one day's — FetchLlmAnalyticsCommand does it once the whole
39+
// batch has finished, rather than four times per hourly run.
3840
Cache::increment(LlmUsageStatsAction::VERSION_CACHE_KEY);
3941

40-
Artisan::call(ClearCommand::class);
41-
4242
return $count;
4343
}
4444
}

app/Console/Commands/FetchLlmAnalyticsCommand.php

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@
88
use Carbon\CarbonImmutable;
99
use Carbon\CarbonInterface;
1010
use Carbon\CarbonPeriod;
11+
use Illuminate\Bus\Batch;
1112
use Illuminate\Console\Command;
13+
use Illuminate\Support\Facades\Bus;
14+
use Spatie\ResponseCache\Facades\ResponseCache;
1215

1316
class FetchLlmAnalyticsCommand extends Command
1417
{
@@ -35,7 +38,19 @@ public function handle(): int
3538

3639
$days = collect(CarbonPeriod::create($from, $to)->toArray());
3740

38-
$days->each(fn (CarbonInterface $day) => FetchLlmUsageJob::dispatch(CarbonImmutable::instance($day)));
41+
// Batched rather than dispatched one by one: the usage figures are rendered
42+
// into the AI pages, so the cached HTML has to go once the numbers change.
43+
// finally() fires after the last day is stored — clearing per job would wipe
44+
// the whole site's response cache once per day in the window, and clearing
45+
// here in the command would fire before the queue had done any work at all.
46+
Bus::batch(
47+
$days->map(fn (CarbonInterface $day): FetchLlmUsageJob => new FetchLlmUsageJob(CarbonImmutable::instance($day)))->all()
48+
)
49+
->name('llm-usage-sync')
50+
->finally(function (Batch $batch): void {
51+
ResponseCache::clear();
52+
})
53+
->dispatch();
3954

4055
$this->info("Dispatched {$days->count()} job(s) for {$from->toDateString()} to {$to->toDateString()}.");
4156

app/Jobs/FetchLlmUsageJob.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@
77
use App\Actions\FetchLlmUsageAction;
88
use App\Actions\StoreLlmUsageAction;
99
use Carbon\CarbonImmutable;
10+
use Illuminate\Bus\Batchable;
1011
use Illuminate\Contracts\Queue\ShouldQueue;
1112
use Illuminate\Foundation\Queue\Queueable;
1213

1314
class FetchLlmUsageJob implements ShouldQueue
1415
{
16+
use Batchable;
1517
use Queueable;
1618

1719
public int $tries = 3;

app/Support/NewsImage.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,23 @@ public static function src(?string $reference, int $width): ?string
4343
return self::fromPublicId($reference, $width);
4444
}
4545

46+
/**
47+
* The og:image counterpart of an SVG hero. Social crawlers cannot render
48+
* SVG, so a same-named PNG rendered from it — see
49+
* public/images/news/placeholders/ — is used when one exists; otherwise
50+
* the caller falls back to the site default image.
51+
*/
52+
public static function ogImage(string $svgReference): ?string
53+
{
54+
if (! self::isLocalPath($svgReference) || ! str_ends_with(strtolower($svgReference), '.svg')) {
55+
return null;
56+
}
57+
58+
$png = substr($svgReference, 0, -4).'.png';
59+
60+
return is_file(public_path(ltrim($png, '/'))) ? asset(ltrim($png, '/')) : null;
61+
}
62+
4663
public static function srcset(?string $reference, int $maxWidth): ?string
4764
{
4865
if (self::src($reference, $maxWidth) === null) {

database/seeders/DatabaseSeeder.php

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,9 @@ public function run(): void
4848
$this->call(AiModelsTableSeeder::class);
4949
$this->call(NetworksTableSeeder::class);
5050
$this->call(NetworkUsersTableSeeder::class);
51+
$this->call(AiModelDailyUsagesTableSeeder::class);
5152

52-
if (app()->isLocal()) {
53-
$this->call(AiModelDailyUsagesTableSeeder::class);
54-
55-
Artisan::call(ClearCommand::class);
56-
}
53+
Artisan::call(ClearCommand::class);
54+
Artisan::call('responsecache:clear');
5755
}
5856
}

phpstan.neon.dist

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,16 @@ parameters:
2121
paths:
2222
- app/Notifications/*
2323

24+
# Mockery's shouldReceive()/expects() are documented as returning a union in
25+
# which only Expectation declares the count modifiers. The calls are correct —
26+
# a mutation check confirms the expectation fails when the behaviour is removed —
27+
# but the stubs cannot express which member of the union is returned.
28+
-
29+
identifier: method.notFound
30+
message: '#Mockery\\(ExpectationInterface|HigherOrderMessage|ExpectsHigherOrderMessage)#'
31+
paths:
32+
- tests/*
33+
2434
excludePaths:
2535
# Vendor-published migration (spatie/laravel-permission) — not our code.
2636
- database/migrations/2024_06_06_100452_create_permission_tables.php
92 KB
Loading
90.6 KB
Loading
92.2 KB
Loading

resources/views/layouts/_partials/_seo.blade.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@
99
@php
1010
// A page image can be a Cloudinary public ID, a remote URL or a path inside
1111
// public/ — only the second form is usable as-is, so NewsImage resolves it to
12-
// an absolute URL. SVG heroes (the local release placeholders) are skipped:
13-
// the social networks do not render SVG, and og:image:type below says PNG.
12+
// an absolute URL. SVG heroes (the local release placeholders) cannot be used
13+
// directly: social networks do not render SVG, and og:image:type below says
14+
// PNG. A same-named PNG rendered from the SVG is used instead when one exists.
1415
$seoImage = str_ends_with(strtolower((string) $page->image), '.svg')
15-
? null
16+
? \App\Support\NewsImage::ogImage($page->image)
1617
: \App\Support\NewsImage::src($page->image, config()->integer('seo.image_width'));
1718
1819
$seoImage ??= url(asset(config('seo.default_image')));

0 commit comments

Comments
 (0)