feat: phase A — backup B2, healthcheck /api/status, vector tiles Tippecanoe

- A1: scripts/backup-postgres.sh — pg_dump quotidien compressé → Backblaze B2 (rclone), rétention 30j
- A2: StatusController retourne HTTP 503 + healthy/alertReason si dernière observation > 6h
- A4: Dockerfile installe tippecanoe, GenerateTilesCommand génère 5 tilesets (obs + h6/12/24/48), Caddyfile sert /tiles/* sans fallback SPA, SargassesMap.jsx passe en sources vector tiles statiques
- chore: backend/public/assets/ ajouté au .gitignore (build artifacts)
- chore: setup vitest frontend + ImpactScoreServiceTest (session précédente)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Gwadaking
2026-04-10 03:40:32 -04:00
parent 1d8771cdcb
commit 9712cb54db
20 changed files with 1653 additions and 872 deletions

View File

@@ -0,0 +1,71 @@
<?php
namespace App\Tests\Unit\Service;
use App\Service\Forecast\ImpactScoreService;
use Doctrine\DBAL\Connection;
use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use App\Service\Push\PushNotificationService;
class ImpactScoreServiceTest extends TestCase
{
private ImpactScoreService $service;
protected function setUp(): void
{
$connection = $this->createStub(Connection::class);
$em = $this->createStub(EntityManagerInterface::class);
$cache = new ArrayAdapter();
$push = $this->createStub(PushNotificationService::class);
$this->service = new ImpactScoreService(
$connection,
$em,
$cache,
new NullLogger(),
$push,
);
}
// ── scoreToLevel ──────────────────────────────────────────────────────────
#[\PHPUnit\Framework\Attributes\DataProvider('scoreLevelProvider')]
public function testScoreToLevel(int $score, string $expected): void
{
$method = new \ReflectionMethod(ImpactScoreService::class, 'scoreToLevel');
$result = $method->invoke($this->service, $score);
self::assertSame($expected, $result);
}
/** @return array<string, array{int, string}> */
public static function scoreLevelProvider(): array
{
return [
'score 0 → low' => [0, 'low'],
'score 30 → low' => [30, 'low'],
'score 31 → medium' => [31, 'medium'],
'score 70 → medium' => [70, 'medium'],
'score 71 → high' => [71, 'high'],
'score 100 → high' => [100, 'high'],
];
}
// ── emptyScore ────────────────────────────────────────────────────────────
public function testEmptyScoreReturnsCorrectStructure(): void
{
$method = new \ReflectionMethod(ImpactScoreService::class, 'emptyScore');
$result = $method->invoke($this->service);
self::assertSame(0, $result['score']);
self::assertSame('low', $result['level']);
self::assertNull($result['distance']);
self::assertSame(0.0, $result['density']);
self::assertSame('stable', $result['trend']);
self::assertNull($result['etaHours']);
}
}