add trend UX + PHPStan level 6 clean (0 errors)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -41,7 +41,8 @@
|
||||
"allow-plugins": {
|
||||
"php-http/discovery": true,
|
||||
"symfony/flex": true,
|
||||
"symfony/runtime": true
|
||||
"symfony/runtime": true,
|
||||
"phpstan/extension-installer": true
|
||||
},
|
||||
"bump-after-update": true,
|
||||
"sort-packages": true
|
||||
@@ -86,5 +87,11 @@
|
||||
"allow-contrib": false,
|
||||
"require": "7.4.*"
|
||||
}
|
||||
},
|
||||
"require-dev": {
|
||||
"phpstan/extension-installer": "^1.4",
|
||||
"phpstan/phpstan": "^2.1",
|
||||
"phpstan/phpstan-doctrine": "^2.0",
|
||||
"phpstan/phpstan-symfony": "^2.0"
|
||||
}
|
||||
}
|
||||
|
||||
256
backend/composer.lock
generated
256
backend/composer.lock
generated
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "c960cf2dbc66a8c555b534d43072fb46",
|
||||
"content-hash": "c745217057712f8ff1cb5cc0fc379c1b",
|
||||
"packages": [
|
||||
{
|
||||
"name": "api-platform/doctrine-common",
|
||||
@@ -8943,7 +8943,259 @@
|
||||
"time": "2022-01-30T20:08:53+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [],
|
||||
"packages-dev": [
|
||||
{
|
||||
"name": "phpstan/extension-installer",
|
||||
"version": "1.4.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpstan/extension-installer.git",
|
||||
"reference": "85e90b3942d06b2326fba0403ec24fe912372936"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/extension-installer/zipball/85e90b3942d06b2326fba0403ec24fe912372936",
|
||||
"reference": "85e90b3942d06b2326fba0403ec24fe912372936",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"composer-plugin-api": "^2.0",
|
||||
"php": "^7.2 || ^8.0",
|
||||
"phpstan/phpstan": "^1.9.0 || ^2.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"composer/composer": "^2.0",
|
||||
"php-parallel-lint/php-parallel-lint": "^1.2.0",
|
||||
"phpstan/phpstan-strict-rules": "^0.11 || ^0.12 || ^1.0"
|
||||
},
|
||||
"type": "composer-plugin",
|
||||
"extra": {
|
||||
"class": "PHPStan\\ExtensionInstaller\\Plugin"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"PHPStan\\ExtensionInstaller\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"description": "Composer plugin for automatic installation of PHPStan extensions",
|
||||
"keywords": [
|
||||
"dev",
|
||||
"static analysis"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/phpstan/extension-installer/issues",
|
||||
"source": "https://github.com/phpstan/extension-installer/tree/1.4.3"
|
||||
},
|
||||
"time": "2024-09-04T20:21:43+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan",
|
||||
"version": "2.1.46",
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/a193923fc2d6325ef4e741cf3af8c3e8f54dbf25",
|
||||
"reference": "a193923fc2d6325ef4e741cf3af8c3e8f54dbf25",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.4|^8.0"
|
||||
},
|
||||
"conflict": {
|
||||
"phpstan/phpstan-shim": "*"
|
||||
},
|
||||
"bin": [
|
||||
"phpstan",
|
||||
"phpstan.phar"
|
||||
],
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"bootstrap.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"description": "PHPStan - PHP Static Analysis Tool",
|
||||
"keywords": [
|
||||
"dev",
|
||||
"static analysis"
|
||||
],
|
||||
"support": {
|
||||
"docs": "https://phpstan.org/user-guide/getting-started",
|
||||
"forum": "https://github.com/phpstan/phpstan/discussions",
|
||||
"issues": "https://github.com/phpstan/phpstan/issues",
|
||||
"security": "https://github.com/phpstan/phpstan/security/policy",
|
||||
"source": "https://github.com/phpstan/phpstan-src"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/ondrejmirtes",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/phpstan",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-04-01T09:25:14+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan-doctrine",
|
||||
"version": "2.0.20",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpstan/phpstan-doctrine.git",
|
||||
"reference": "72f4f7a02d6c98d9101e8616e0488bc0a785196d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/72f4f7a02d6c98d9101e8616e0488bc0a785196d",
|
||||
"reference": "72f4f7a02d6c98d9101e8616e0488bc0a785196d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.4 || ^8.0",
|
||||
"phpstan/phpstan": "^2.1.34"
|
||||
},
|
||||
"conflict": {
|
||||
"doctrine/collections": "<1.0",
|
||||
"doctrine/common": "<2.7",
|
||||
"doctrine/mongodb-odm": "<1.2",
|
||||
"doctrine/orm": "<2.5",
|
||||
"doctrine/persistence": "<1.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"cache/array-adapter": "^1.1",
|
||||
"composer/semver": "^3.3.2",
|
||||
"cweagans/composer-patches": "^1.7.3",
|
||||
"doctrine/annotations": "^2.0",
|
||||
"doctrine/collections": "^1.6 || ^2.1",
|
||||
"doctrine/common": "^2.7 || ^3.0",
|
||||
"doctrine/dbal": "^3.3.8",
|
||||
"doctrine/lexer": "^2.0 || ^3.0",
|
||||
"doctrine/mongodb-odm": "^2.4.3",
|
||||
"doctrine/orm": "^2.16.0",
|
||||
"doctrine/persistence": "^2.2.1 || ^3.4.3",
|
||||
"gedmo/doctrine-extensions": "^3.8",
|
||||
"nesbot/carbon": "^2.49",
|
||||
"php-parallel-lint/php-parallel-lint": "^1.2",
|
||||
"phpstan/phpstan-deprecation-rules": "^2.0.2",
|
||||
"phpstan/phpstan-phpunit": "^2.0.8",
|
||||
"phpstan/phpstan-strict-rules": "^2.0",
|
||||
"phpunit/phpunit": "^9.6.20",
|
||||
"ramsey/uuid": "^4.2",
|
||||
"symfony/cache": "^5.4",
|
||||
"symfony/uid": "^5.4 || ^6.4 || ^7.3"
|
||||
},
|
||||
"type": "phpstan-extension",
|
||||
"extra": {
|
||||
"phpstan": {
|
||||
"includes": [
|
||||
"extension.neon",
|
||||
"rules.neon"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"PHPStan\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"description": "Doctrine extensions for PHPStan",
|
||||
"keywords": [
|
||||
"static analysis"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/phpstan/phpstan-doctrine/issues",
|
||||
"source": "https://github.com/phpstan/phpstan-doctrine/tree/2.0.20"
|
||||
},
|
||||
"time": "2026-03-13T13:44:51+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan-symfony",
|
||||
"version": "2.0.15",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpstan/phpstan-symfony.git",
|
||||
"reference": "9b85ab476969b87bbe2253b69e265a9359b2f395"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/9b85ab476969b87bbe2253b69e265a9359b2f395",
|
||||
"reference": "9b85ab476969b87bbe2253b69e265a9359b2f395",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-simplexml": "*",
|
||||
"php": "^7.4 || ^8.0",
|
||||
"phpstan/phpstan": "^2.1.13"
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/framework-bundle": "<3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"php-parallel-lint/php-parallel-lint": "^1.2",
|
||||
"phpstan/phpstan-phpunit": "^2.0.8",
|
||||
"phpstan/phpstan-strict-rules": "^2.0",
|
||||
"phpunit/phpunit": "^9.6",
|
||||
"psr/container": "1.1.2",
|
||||
"symfony/config": "^5.4 || ^6.1",
|
||||
"symfony/console": "^5.4 || ^6.1",
|
||||
"symfony/dependency-injection": "^5.4 || ^6.1",
|
||||
"symfony/form": "^5.4 || ^6.1",
|
||||
"symfony/framework-bundle": "^5.4 || ^6.1",
|
||||
"symfony/http-foundation": "^5.4 || ^6.1",
|
||||
"symfony/messenger": "^5.4",
|
||||
"symfony/polyfill-php80": "^1.24",
|
||||
"symfony/serializer": "^5.4",
|
||||
"symfony/service-contracts": "^2.2.0"
|
||||
},
|
||||
"type": "phpstan-extension",
|
||||
"extra": {
|
||||
"phpstan": {
|
||||
"includes": [
|
||||
"extension.neon",
|
||||
"rules.neon"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"PHPStan\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Lukáš Unger",
|
||||
"email": "looky.msc@gmail.com",
|
||||
"homepage": "https://lookyman.net"
|
||||
}
|
||||
],
|
||||
"description": "Symfony Framework extensions and rules for PHPStan",
|
||||
"keywords": [
|
||||
"static analysis"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/phpstan/phpstan-symfony/issues",
|
||||
"source": "https://github.com/phpstan/phpstan-symfony/tree/2.0.15"
|
||||
},
|
||||
"time": "2026-02-26T10:15:59+00:00"
|
||||
}
|
||||
],
|
||||
"aliases": [],
|
||||
"minimum-stability": "stable",
|
||||
"stability-flags": {},
|
||||
|
||||
8
backend/phpstan.neon
Normal file
8
backend/phpstan.neon
Normal file
@@ -0,0 +1,8 @@
|
||||
parameters:
|
||||
level: 6
|
||||
paths:
|
||||
- src
|
||||
symfony:
|
||||
containerXmlPath: var/cache/dev/App_KernelDevDebugContainer.xml
|
||||
doctrine:
|
||||
objectManagerLoader: tests/bootstrap_phpstan.php
|
||||
@@ -63,6 +63,8 @@ class SpotScoreController extends AbstractController
|
||||
|
||||
/**
|
||||
* Score à un instant précis (sans cache).
|
||||
*
|
||||
* @return array{value: int, level: string, distance: float|null, density: float|null, trend: string|null, computedAt: string}|null
|
||||
*/
|
||||
private function loadScoreAt(string $spotId, \DateTimeImmutable $at): ?array
|
||||
{
|
||||
@@ -81,6 +83,8 @@ class SpotScoreController extends AbstractController
|
||||
/**
|
||||
* Scores estimés par horizon à partir des forecasts les plus récents.
|
||||
* Retourne null si aucun forecast disponible.
|
||||
*
|
||||
* @return array<string, array{score: int, level: string, distanceKm: float, confidence: float, validAt: string}>|null
|
||||
*/
|
||||
private function loadHorizonScores(string $spotId): ?array
|
||||
{
|
||||
@@ -125,6 +129,10 @@ class SpotScoreController extends AbstractController
|
||||
return $horizons;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $row
|
||||
* @return array{value: int, level: string, distance: float|null, density: float|null, trend: string|null, computedAt: string}
|
||||
*/
|
||||
private function formatScore(array $row): array
|
||||
{
|
||||
return [
|
||||
|
||||
@@ -36,33 +36,38 @@ class CoastalPoint
|
||||
|
||||
#[ORM\Column(length: 150)]
|
||||
#[Groups(['spot:read'])]
|
||||
private ?string $name = null;
|
||||
private string $name;
|
||||
|
||||
#[ORM\Column(type: 'geometry', options: ['geometry_type' => 'POINT', 'srid' => 4326])]
|
||||
private mixed $geometry = null;
|
||||
|
||||
#[ORM\Column(length: 30)]
|
||||
#[Groups(['spot:read'])]
|
||||
private ?string $type = null;
|
||||
private string $type;
|
||||
|
||||
#[ORM\Column(length: 100)]
|
||||
#[Groups(['spot:read'])]
|
||||
private ?string $region = null;
|
||||
private string $region;
|
||||
|
||||
/** @var Collection<int, ImpactScore> */
|
||||
#[ORM\OneToMany(targetEntity: ImpactScore::class, mappedBy: 'coastalPoint')]
|
||||
private Collection $impactScores;
|
||||
|
||||
/** @var Collection<int, UserFeedback> */
|
||||
#[ORM\OneToMany(targetEntity: UserFeedback::class, mappedBy: 'coastalPoint')]
|
||||
private Collection $feedbacks;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->name = '';
|
||||
$this->type = '';
|
||||
$this->region = '';
|
||||
$this->impactScores = new ArrayCollection();
|
||||
$this->feedbacks = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?Uuid { return $this->id; }
|
||||
public function getName(): ?string { return $this->name; }
|
||||
public function getName(): string { return $this->name; }
|
||||
public function setName(string $name): static { $this->name = $name; return $this; }
|
||||
public function getGeometry(): mixed { return $this->geometry; }
|
||||
public function setGeometry(mixed $geometry): static { $this->geometry = $geometry; return $this; }
|
||||
@@ -82,9 +87,9 @@ class CoastalPoint
|
||||
preg_match('/POINT\([^ ]+ ([^)]+)\)/', (string) $this->geometry, $m);
|
||||
return isset($m[1]) ? (float) $m[1] : null;
|
||||
}
|
||||
public function getType(): ?string { return $this->type; }
|
||||
public function getType(): string { return $this->type; }
|
||||
public function setType(string $type): static { $this->type = $type; return $this; }
|
||||
public function getRegion(): ?string { return $this->region; }
|
||||
public function getRegion(): string { return $this->region; }
|
||||
public function setRegion(string $region): static { $this->region = $region; return $this; }
|
||||
|
||||
/** @return Collection<int, ImpactScore> */
|
||||
|
||||
@@ -19,13 +19,13 @@ class DataIngestionJob
|
||||
private ?Uuid $id = null;
|
||||
|
||||
#[ORM\Column(length: 50)]
|
||||
private ?string $source = null;
|
||||
private string $source;
|
||||
|
||||
#[ORM\Column(length: 100)]
|
||||
private ?string $tileId = null;
|
||||
private string $tileId;
|
||||
|
||||
#[ORM\Column]
|
||||
private ?\DateTimeImmutable $requestedAt = null;
|
||||
private \DateTimeImmutable $requestedAt;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?\DateTimeImmutable $processedAt = null;
|
||||
@@ -41,15 +41,17 @@ class DataIngestionJob
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->source = '';
|
||||
$this->tileId = '';
|
||||
$this->requestedAt = new \DateTimeImmutable();
|
||||
}
|
||||
|
||||
public function getId(): ?Uuid { return $this->id; }
|
||||
public function getSource(): ?string { return $this->source; }
|
||||
public function getSource(): string { return $this->source; }
|
||||
public function setSource(string $source): static { $this->source = $source; return $this; }
|
||||
public function getTileId(): ?string { return $this->tileId; }
|
||||
public function getTileId(): string { return $this->tileId; }
|
||||
public function setTileId(string $tileId): static { $this->tileId = $tileId; return $this; }
|
||||
public function getRequestedAt(): ?\DateTimeImmutable { return $this->requestedAt; }
|
||||
public function getRequestedAt(): \DateTimeImmutable { return $this->requestedAt; }
|
||||
public function getProcessedAt(): ?\DateTimeImmutable { return $this->processedAt; }
|
||||
public function setProcessedAt(?\DateTimeImmutable $processedAt): static { $this->processedAt = $processedAt; return $this; }
|
||||
public function getStatus(): string { return $this->status; }
|
||||
|
||||
@@ -29,16 +29,16 @@ class ImpactScore
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'impactScores')]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?CoastalPoint $coastalPoint = null;
|
||||
private CoastalPoint $coastalPoint;
|
||||
|
||||
#[ORM\Column]
|
||||
private ?\DateTimeImmutable $timestamp = null;
|
||||
private \DateTimeImmutable $timestamp;
|
||||
|
||||
#[ORM\Column]
|
||||
private ?int $score = null;
|
||||
private int $score;
|
||||
|
||||
#[ORM\Column(length: 20)]
|
||||
private ?string $level = null;
|
||||
private string $level;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?float $distanceToNearestSargassum = null;
|
||||
@@ -52,16 +52,18 @@ class ImpactScore
|
||||
public function __construct()
|
||||
{
|
||||
$this->timestamp = new \DateTimeImmutable();
|
||||
$this->score = 0;
|
||||
$this->level = 'low';
|
||||
}
|
||||
|
||||
public function getId(): ?Uuid { return $this->id; }
|
||||
public function getCoastalPoint(): ?CoastalPoint { return $this->coastalPoint; }
|
||||
public function setCoastalPoint(?CoastalPoint $coastalPoint): static { $this->coastalPoint = $coastalPoint; return $this; }
|
||||
public function getTimestamp(): ?\DateTimeImmutable { return $this->timestamp; }
|
||||
public function getCoastalPoint(): CoastalPoint { return $this->coastalPoint; }
|
||||
public function setCoastalPoint(CoastalPoint $coastalPoint): static { $this->coastalPoint = $coastalPoint; return $this; }
|
||||
public function getTimestamp(): \DateTimeImmutable { return $this->timestamp; }
|
||||
public function setTimestamp(\DateTimeImmutable $timestamp): static { $this->timestamp = $timestamp; return $this; }
|
||||
public function getScore(): ?int { return $this->score; }
|
||||
public function getScore(): int { return $this->score; }
|
||||
public function setScore(int $score): static { $this->score = $score; return $this; }
|
||||
public function getLevel(): ?string { return $this->level; }
|
||||
public function getLevel(): string { return $this->level; }
|
||||
public function setLevel(string $level): static { $this->level = $level; return $this; }
|
||||
public function getDistanceToNearestSargassum(): ?float { return $this->distanceToNearestSargassum; }
|
||||
public function setDistanceToNearestSargassum(?float $d): static { $this->distanceToNearestSargassum = $d; return $this; }
|
||||
|
||||
@@ -18,34 +18,37 @@ class PushSubscription
|
||||
private ?Uuid $id = null;
|
||||
|
||||
#[ORM\Column(type: 'text', unique: true)]
|
||||
private ?string $endpoint = null;
|
||||
private string $endpoint;
|
||||
|
||||
#[ORM\Column(type: 'text')]
|
||||
private ?string $authToken = null;
|
||||
private string $authToken;
|
||||
|
||||
#[ORM\Column(type: 'text')]
|
||||
private ?string $p256dhKey = null;
|
||||
private string $p256dhKey;
|
||||
|
||||
#[ORM\ManyToOne]
|
||||
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
|
||||
private ?CoastalPoint $coastalPoint = null;
|
||||
|
||||
#[ORM\Column]
|
||||
private ?\DateTimeImmutable $createdAt = null;
|
||||
private \DateTimeImmutable $createdAt;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->endpoint = '';
|
||||
$this->authToken = '';
|
||||
$this->p256dhKey = '';
|
||||
$this->createdAt = new \DateTimeImmutable();
|
||||
}
|
||||
|
||||
public function getId(): ?Uuid { return $this->id; }
|
||||
public function getEndpoint(): ?string { return $this->endpoint; }
|
||||
public function getEndpoint(): string { return $this->endpoint; }
|
||||
public function setEndpoint(string $endpoint): static { $this->endpoint = $endpoint; return $this; }
|
||||
public function getAuthToken(): ?string { return $this->authToken; }
|
||||
public function getAuthToken(): string { return $this->authToken; }
|
||||
public function setAuthToken(string $authToken): static { $this->authToken = $authToken; return $this; }
|
||||
public function getP256dhKey(): ?string { return $this->p256dhKey; }
|
||||
public function getP256dhKey(): string { return $this->p256dhKey; }
|
||||
public function setP256dhKey(string $p256dhKey): static { $this->p256dhKey = $p256dhKey; return $this; }
|
||||
public function getCoastalPoint(): ?CoastalPoint { return $this->coastalPoint; }
|
||||
public function setCoastalPoint(?CoastalPoint $coastalPoint): static { $this->coastalPoint = $coastalPoint; return $this; }
|
||||
public function getCreatedAt(): ?\DateTimeImmutable { return $this->createdAt; }
|
||||
public function getCreatedAt(): \DateTimeImmutable { return $this->createdAt; }
|
||||
}
|
||||
|
||||
@@ -30,54 +30,58 @@ class SargassumForecast
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'forecasts')]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?SargassumObservation $sourceObservation = null;
|
||||
private SargassumObservation $sourceObservation;
|
||||
|
||||
#[ORM\Column(type: 'geometry', options: ['geometry_type' => 'MULTIPOLYGON', 'srid' => 4326])]
|
||||
private mixed $geometry = null;
|
||||
|
||||
#[ORM\Column]
|
||||
private ?\DateTimeImmutable $validAt = null;
|
||||
private \DateTimeImmutable $validAt;
|
||||
|
||||
#[ORM\Column]
|
||||
private ?\DateTimeImmutable $computedAt = null;
|
||||
private \DateTimeImmutable $computedAt;
|
||||
|
||||
#[ORM\Column(length: 50)]
|
||||
private ?string $modelVersion = null;
|
||||
private string $modelVersion;
|
||||
|
||||
#[ORM\Column]
|
||||
private ?int $timeHorizon = null;
|
||||
private int $timeHorizon;
|
||||
|
||||
#[ORM\Column]
|
||||
private ?float $confidence = null;
|
||||
private float $confidence;
|
||||
|
||||
#[ORM\Column(type: 'geometry', nullable: true, options: ['geometry_type' => 'POINT', 'srid' => 4326])]
|
||||
private mixed $driftVectorAvg = null;
|
||||
|
||||
#[ORM\Column]
|
||||
private ?\DateTimeImmutable $createdAt = null;
|
||||
private \DateTimeImmutable $createdAt;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->createdAt = new \DateTimeImmutable();
|
||||
$this->validAt = new \DateTimeImmutable();
|
||||
$this->computedAt = new \DateTimeImmutable();
|
||||
$this->createdAt = new \DateTimeImmutable();
|
||||
$this->modelVersion = '';
|
||||
$this->timeHorizon = 0;
|
||||
$this->confidence = 0.0;
|
||||
}
|
||||
|
||||
public function getId(): ?Uuid { return $this->id; }
|
||||
public function getSourceObservation(): ?SargassumObservation { return $this->sourceObservation; }
|
||||
public function setSourceObservation(?SargassumObservation $sourceObservation): static { $this->sourceObservation = $sourceObservation; return $this; }
|
||||
public function getSourceObservation(): SargassumObservation { return $this->sourceObservation; }
|
||||
public function setSourceObservation(SargassumObservation $sourceObservation): static { $this->sourceObservation = $sourceObservation; return $this; }
|
||||
public function getGeometry(): mixed { return $this->geometry; }
|
||||
public function setGeometry(mixed $geometry): static { $this->geometry = $geometry; return $this; }
|
||||
public function getValidAt(): ?\DateTimeImmutable { return $this->validAt; }
|
||||
public function getValidAt(): \DateTimeImmutable { return $this->validAt; }
|
||||
public function setValidAt(\DateTimeImmutable $validAt): static { $this->validAt = $validAt; return $this; }
|
||||
public function getComputedAt(): ?\DateTimeImmutable { return $this->computedAt; }
|
||||
public function getComputedAt(): \DateTimeImmutable { return $this->computedAt; }
|
||||
public function setComputedAt(\DateTimeImmutable $computedAt): static { $this->computedAt = $computedAt; return $this; }
|
||||
public function getModelVersion(): ?string { return $this->modelVersion; }
|
||||
public function getModelVersion(): string { return $this->modelVersion; }
|
||||
public function setModelVersion(string $modelVersion): static { $this->modelVersion = $modelVersion; return $this; }
|
||||
public function getTimeHorizon(): ?int { return $this->timeHorizon; }
|
||||
public function getTimeHorizon(): int { return $this->timeHorizon; }
|
||||
public function setTimeHorizon(int $timeHorizon): static { $this->timeHorizon = $timeHorizon; return $this; }
|
||||
public function getConfidence(): ?float { return $this->confidence; }
|
||||
public function getConfidence(): float { return $this->confidence; }
|
||||
public function setConfidence(float $confidence): static { $this->confidence = $confidence; return $this; }
|
||||
public function getDriftVectorAvg(): mixed { return $this->driftVectorAvg; }
|
||||
public function setDriftVectorAvg(mixed $driftVectorAvg): static { $this->driftVectorAvg = $driftVectorAvg; return $this; }
|
||||
public function getCreatedAt(): ?\DateTimeImmutable { return $this->createdAt; }
|
||||
public function getCreatedAt(): \DateTimeImmutable { return $this->createdAt; }
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ use App\Repository\SargassumObservationRepository;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Jsor\Doctrine\PostGIS\Types\PostGISType;
|
||||
use Symfony\Bridge\Doctrine\Types\UuidType;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
|
||||
@@ -28,25 +27,25 @@ class SargassumObservation
|
||||
private mixed $bbox = null;
|
||||
|
||||
#[ORM\Column]
|
||||
private ?\DateTimeImmutable $detectedAt = null;
|
||||
private \DateTimeImmutable $detectedAt;
|
||||
|
||||
#[ORM\Column(length: 50)]
|
||||
private ?string $source = null;
|
||||
private string $source;
|
||||
|
||||
#[ORM\Column(length: 100)]
|
||||
private ?string $tileId = null;
|
||||
private string $tileId;
|
||||
|
||||
#[ORM\Column]
|
||||
private ?float $cloudCoverage = null;
|
||||
private float $cloudCoverage;
|
||||
|
||||
#[ORM\Column]
|
||||
private ?float $confidence = null;
|
||||
private float $confidence;
|
||||
|
||||
#[ORM\Column]
|
||||
private ?float $afaiMean = null;
|
||||
private float $afaiMean;
|
||||
|
||||
#[ORM\Column]
|
||||
private ?float $afaiStd = null;
|
||||
private float $afaiStd;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?float $coverageArea = null;
|
||||
@@ -55,15 +54,23 @@ class SargassumObservation
|
||||
private ?string $processingVersion = null;
|
||||
|
||||
#[ORM\Column]
|
||||
private ?\DateTimeImmutable $createdAt = null;
|
||||
private \DateTimeImmutable $createdAt;
|
||||
|
||||
/** @var Collection<int, SargassumForecast> */
|
||||
#[ORM\OneToMany(targetEntity: SargassumForecast::class, mappedBy: 'sourceObservation')]
|
||||
private Collection $forecasts;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->forecasts = new ArrayCollection();
|
||||
$this->detectedAt = new \DateTimeImmutable();
|
||||
$this->source = '';
|
||||
$this->tileId = '';
|
||||
$this->cloudCoverage = 0.0;
|
||||
$this->confidence = 0.0;
|
||||
$this->afaiMean = 0.0;
|
||||
$this->afaiStd = 0.0;
|
||||
$this->createdAt = new \DateTimeImmutable();
|
||||
$this->forecasts = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?Uuid { return $this->id; }
|
||||
@@ -71,25 +78,25 @@ class SargassumObservation
|
||||
public function setGeometry(mixed $geometry): static { $this->geometry = $geometry; return $this; }
|
||||
public function getBbox(): mixed { return $this->bbox; }
|
||||
public function setBbox(mixed $bbox): static { $this->bbox = $bbox; return $this; }
|
||||
public function getDetectedAt(): ?\DateTimeImmutable { return $this->detectedAt; }
|
||||
public function getDetectedAt(): \DateTimeImmutable { return $this->detectedAt; }
|
||||
public function setDetectedAt(\DateTimeImmutable $detectedAt): static { $this->detectedAt = $detectedAt; return $this; }
|
||||
public function getSource(): ?string { return $this->source; }
|
||||
public function getSource(): string { return $this->source; }
|
||||
public function setSource(string $source): static { $this->source = $source; return $this; }
|
||||
public function getTileId(): ?string { return $this->tileId; }
|
||||
public function getTileId(): string { return $this->tileId; }
|
||||
public function setTileId(string $tileId): static { $this->tileId = $tileId; return $this; }
|
||||
public function getCloudCoverage(): ?float { return $this->cloudCoverage; }
|
||||
public function getCloudCoverage(): float { return $this->cloudCoverage; }
|
||||
public function setCloudCoverage(float $cloudCoverage): static { $this->cloudCoverage = $cloudCoverage; return $this; }
|
||||
public function getConfidence(): ?float { return $this->confidence; }
|
||||
public function getConfidence(): float { return $this->confidence; }
|
||||
public function setConfidence(float $confidence): static { $this->confidence = $confidence; return $this; }
|
||||
public function getAfaiMean(): ?float { return $this->afaiMean; }
|
||||
public function getAfaiMean(): float { return $this->afaiMean; }
|
||||
public function setAfaiMean(float $afaiMean): static { $this->afaiMean = $afaiMean; return $this; }
|
||||
public function getAfaiStd(): ?float { return $this->afaiStd; }
|
||||
public function getAfaiStd(): float { return $this->afaiStd; }
|
||||
public function setAfaiStd(float $afaiStd): static { $this->afaiStd = $afaiStd; return $this; }
|
||||
public function getCoverageArea(): ?float { return $this->coverageArea; }
|
||||
public function setCoverageArea(?float $coverageArea): static { $this->coverageArea = $coverageArea; return $this; }
|
||||
public function getProcessingVersion(): ?string { return $this->processingVersion; }
|
||||
public function setProcessingVersion(?string $processingVersion): static { $this->processingVersion = $processingVersion; return $this; }
|
||||
public function getCreatedAt(): ?\DateTimeImmutable { return $this->createdAt; }
|
||||
public function getCreatedAt(): \DateTimeImmutable { return $this->createdAt; }
|
||||
|
||||
/** @return Collection<int, SargassumForecast> */
|
||||
public function getForecasts(): Collection { return $this->forecasts; }
|
||||
|
||||
@@ -25,10 +25,10 @@ class UserFeedback
|
||||
private ?CoastalPoint $coastalPoint = null;
|
||||
|
||||
#[ORM\Column]
|
||||
private ?\DateTimeImmutable $timestamp = null;
|
||||
private \DateTimeImmutable $timestamp;
|
||||
|
||||
#[ORM\Column]
|
||||
private ?bool $hasSeaweed = null;
|
||||
private bool $hasSeaweed;
|
||||
|
||||
#[ORM\Column(length: 20, nullable: true)]
|
||||
private ?string $density = null;
|
||||
@@ -39,6 +39,7 @@ class UserFeedback
|
||||
public function __construct()
|
||||
{
|
||||
$this->timestamp = new \DateTimeImmutable();
|
||||
$this->hasSeaweed = false;
|
||||
}
|
||||
|
||||
public function getId(): ?Uuid { return $this->id; }
|
||||
@@ -46,9 +47,9 @@ class UserFeedback
|
||||
public function setLocation(mixed $location): static { $this->location = $location; return $this; }
|
||||
public function getCoastalPoint(): ?CoastalPoint { return $this->coastalPoint; }
|
||||
public function setCoastalPoint(?CoastalPoint $coastalPoint): static { $this->coastalPoint = $coastalPoint; return $this; }
|
||||
public function getTimestamp(): ?\DateTimeImmutable { return $this->timestamp; }
|
||||
public function getTimestamp(): \DateTimeImmutable { return $this->timestamp; }
|
||||
public function setTimestamp(\DateTimeImmutable $timestamp): static { $this->timestamp = $timestamp; return $this; }
|
||||
public function getHasSeaweed(): ?bool { return $this->hasSeaweed; }
|
||||
public function getHasSeaweed(): bool { return $this->hasSeaweed; }
|
||||
public function setHasSeaweed(bool $hasSeaweed): static { $this->hasSeaweed = $hasSeaweed; return $this; }
|
||||
public function getDensity(): ?string { return $this->density; }
|
||||
public function setDensity(?string $density): static { $this->density = $density; return $this; }
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Entity\CoastalPoint;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/** @extends ServiceEntityRepository<CoastalPoint> */
|
||||
class CoastalPointRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Entity\DataIngestionJob;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/** @extends ServiceEntityRepository<DataIngestionJob> */
|
||||
class DataIngestionJobRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Entity\ImpactScore;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/** @extends ServiceEntityRepository<ImpactScore> */
|
||||
class ImpactScoreRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Entity\PushSubscription;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/** @extends ServiceEntityRepository<PushSubscription> */
|
||||
class PushSubscriptionRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Entity\SargassumForecast;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/** @extends ServiceEntityRepository<SargassumForecast> */
|
||||
class SargassumForecastRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Entity\SargassumObservation;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/** @extends ServiceEntityRepository<SargassumObservation> */
|
||||
class SargassumObservationRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Entity\UserFeedback;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/** @extends ServiceEntityRepository<UserFeedback> */
|
||||
class UserFeedbackRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
|
||||
@@ -102,6 +102,7 @@ class DriftSimulationService
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/** @return array{lat: float, lng: float}|null */
|
||||
private function getCentroid(string $observationId): ?array
|
||||
{
|
||||
$row = $this->connection->fetchAssociative(
|
||||
@@ -167,6 +168,7 @@ class DriftSimulationService
|
||||
/**
|
||||
* Reconstruit un MULTIPOLYGON depuis un nuage de points via PostGIS.
|
||||
*/
|
||||
/** @param array<int, array{lat: float, lng: float}> $points */
|
||||
private function reconstructPolygon(array $points): ?string
|
||||
{
|
||||
if (count($points) < 3) {
|
||||
@@ -196,6 +198,10 @@ class DriftSimulationService
|
||||
/**
|
||||
* Calcule le vecteur de dérive moyen (composantes lat/lng) sur une période.
|
||||
*/
|
||||
/**
|
||||
* @param array<int, array{speed: float, direction: float}> $windData
|
||||
* @return array{lat: float, lng: float}
|
||||
*/
|
||||
private function avgDriftVector(array $windData, int $fromHour, int $toHour): array
|
||||
{
|
||||
$count = $toHour - $fromHour;
|
||||
|
||||
@@ -49,6 +49,8 @@ class ImpactScoreService
|
||||
/**
|
||||
* Calcule et sauvegarde les scores pour tous les CoastalPoints
|
||||
* à partir d'une observation et de ses forecasts.
|
||||
*
|
||||
* @param array<int, SargassumForecast> $forecasts
|
||||
*/
|
||||
public function computeForObservation(SargassumObservation $observation, array $forecasts): void
|
||||
{
|
||||
@@ -87,6 +89,8 @@ class ImpactScoreService
|
||||
|
||||
/**
|
||||
* Retourne le score mis en cache pour un spot (TTL 3h).
|
||||
*
|
||||
* @return array{value: int, level: string, distance: float|null, density: float|null, trend: string|null, computedAt: string}|null
|
||||
*/
|
||||
public function getCachedScore(string $spotId): ?array
|
||||
{
|
||||
@@ -100,6 +104,7 @@ class ImpactScoreService
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/** @return array{score: int, level: string, distance: float|null, density: float, trend: string} */
|
||||
private function computeScore(CoastalPoint $spot, string $obsId, ?SargassumForecast $forecastH6): array
|
||||
{
|
||||
$spotId = (string) $spot->getId();
|
||||
@@ -194,13 +199,21 @@ class ImpactScoreService
|
||||
|
||||
private function getPreviousDistance(string $spotId): ?float
|
||||
{
|
||||
// On cherche le score de la précédente "session" d'ingestion :
|
||||
// au moins 6h d'ancienneté pour éviter de comparer avec un recalcul immédiat,
|
||||
// et au plus 14 jours pour rester pertinent.
|
||||
$since = (new \DateTimeImmutable())->modify('-14 days')->format('Y-m-d H:i:s');
|
||||
$before = (new \DateTimeImmutable())->modify('-6 hours')->format('Y-m-d H:i:s');
|
||||
|
||||
$row = $this->connection->fetchAssociative(
|
||||
'SELECT distance_to_nearest_sargassum
|
||||
FROM impact_score
|
||||
WHERE coastal_point_id = :spotId
|
||||
AND timestamp <= :before
|
||||
AND timestamp >= :since
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT 1',
|
||||
['spotId' => $spotId]
|
||||
['spotId' => $spotId, 'before' => $before, 'since' => $since]
|
||||
);
|
||||
|
||||
return ($row !== false && $row['distance_to_nearest_sargassum'] !== null)
|
||||
@@ -208,6 +221,7 @@ class ImpactScoreService
|
||||
: null;
|
||||
}
|
||||
|
||||
/** @param array{score: int, level: string, distance: float|null, density: float, trend: string} $data */
|
||||
private function persistScore(CoastalPoint $spot, array $data): void
|
||||
{
|
||||
$s = new ImpactScore();
|
||||
@@ -221,6 +235,7 @@ class ImpactScoreService
|
||||
$this->em->persist($s);
|
||||
}
|
||||
|
||||
/** @return array{value: int, level: string, distance: float|null, density: float|null, trend: string|null, computedAt: string}|null */
|
||||
private function loadLatestScore(string $spotId): ?array
|
||||
{
|
||||
$row = $this->connection->fetchAssociative(
|
||||
@@ -265,6 +280,7 @@ class ImpactScoreService
|
||||
};
|
||||
}
|
||||
|
||||
/** @return array{score: int, level: string, distance: null, density: float, trend: string} */
|
||||
private function emptyScore(): array
|
||||
{
|
||||
return ['score' => 0, 'level' => 'low', 'distance' => null, 'density' => 0.0, 'trend' => 'stable'];
|
||||
@@ -291,10 +307,11 @@ class ImpactScoreService
|
||||
return ($row !== false && (int) $row['positive_count'] > 0) ? (float) self::FEEDBACK_BONUS : 0.0;
|
||||
}
|
||||
|
||||
/** @param array<int, SargassumForecast> $forecasts */
|
||||
private function findForecastByHorizon(array $forecasts, int $horizon): ?SargassumForecast
|
||||
{
|
||||
foreach ($forecasts as $f) {
|
||||
if ($f instanceof SargassumForecast && $f->getTimeHorizon() === $horizon) {
|
||||
if ($f->getTimeHorizon() === $horizon) {
|
||||
return $f;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ class IngestionService
|
||||
* Tente l'ingestion Sentinel-2, puis Sentinel-3 OLCI en fallback si la
|
||||
* couverture nuageuse est trop élevée ou si la scène S2 est indisponible.
|
||||
*/
|
||||
/** @param array<int, float> $bbox */
|
||||
public function ingestWithFallback(array $bbox, \DateTimeImmutable $date): void
|
||||
{
|
||||
try {
|
||||
@@ -41,6 +42,7 @@ class IngestionService
|
||||
}
|
||||
}
|
||||
|
||||
/** @param array<int, float> $bbox */
|
||||
public function ingest(
|
||||
array $bbox,
|
||||
\DateTimeImmutable $date,
|
||||
@@ -177,6 +179,9 @@ class IngestionService
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{type: string, coordinates: array<int, mixed>}|null
|
||||
*/
|
||||
private function buildMultiPolygon(string $geojsonPath): ?array
|
||||
{
|
||||
if (!file_exists($geojsonPath)) {
|
||||
|
||||
@@ -48,6 +48,7 @@ class SentinelHubClient
|
||||
* Retourne la couverture nuageuse (%) de la meilleure scène disponible.
|
||||
* collection : 'sentinel-2-l2a' (défaut) ou 'sentinel-3-olci'
|
||||
*/
|
||||
/** @param array<int, float> $bbox */
|
||||
public function getCloudCoverage(array $bbox, \DateTimeImmutable $date, string $collection = 'sentinel-2-l2a'): float
|
||||
{
|
||||
// Sentinel-3 OLCI n'expose pas eo:cloud_cover dans le Catalog ; on renvoie 0
|
||||
@@ -83,6 +84,10 @@ class SentinelHubClient
|
||||
* calculé via evalscript AFAI/FAI, et le sauvegarde dans $outputPath.
|
||||
* collection : 'sentinel-2-l2a' (défaut) ou 'sentinel-3-olci'
|
||||
*/
|
||||
/**
|
||||
* @param array<int, float> $bbox
|
||||
* @return array{tileId: string, afaiMean: float, afaiStd: float}
|
||||
*/
|
||||
public function downloadAfaiBinary(
|
||||
array $bbox,
|
||||
\DateTimeImmutable $date,
|
||||
|
||||
@@ -70,6 +70,15 @@
|
||||
"config/packages/nelmio_cors.yaml"
|
||||
]
|
||||
},
|
||||
"phpstan/phpstan": {
|
||||
"version": "2.1",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes-contrib",
|
||||
"branch": "main",
|
||||
"version": "1.0",
|
||||
"ref": "5e490cc197fb6bb1ae22e5abbc531ddc633b6767"
|
||||
}
|
||||
},
|
||||
"symfony/console": {
|
||||
"version": "7.4",
|
||||
"recipe": {
|
||||
|
||||
13
backend/tests/bootstrap_phpstan.php
Normal file
13
backend/tests/bootstrap_phpstan.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
use App\Kernel;
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
// Variables minimales pour booter le kernel sans vraie DB
|
||||
$_ENV['DATABASE_URL'] = $_ENV['DATABASE_URL'] ?? 'postgresql://user:pass@localhost:5432/phpstan_dummy?serverVersion=16';
|
||||
$_ENV['APP_SECRET'] = $_ENV['APP_SECRET'] ?? 'phpstan-dummy-secret';
|
||||
$_SERVER['DATABASE_URL'] = $_ENV['DATABASE_URL'];
|
||||
$_SERVER['APP_SECRET'] = $_ENV['APP_SECRET'];
|
||||
|
||||
$kernel = new Kernel('dev', true);
|
||||
$kernel->boot();
|
||||
return $kernel->getContainer()->get('doctrine')->getManager();
|
||||
@@ -124,6 +124,25 @@
|
||||
.gauge--medium .spot-panel__gauge-fill { background: #eab308; }
|
||||
.gauge--high .spot-panel__gauge-fill { background: #ef4444; }
|
||||
|
||||
/* Trend */
|
||||
.spot-panel__trend {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 9px 14px;
|
||||
border-radius: 12px;
|
||||
border: 2px solid rgba(0,0,0,0.12);
|
||||
margin-bottom: 12px;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
.trend--bad { background: #fee2e2; color: #b91c1c; }
|
||||
.trend--neutral { background: #f1f5f9; color: #475569; }
|
||||
.trend--good { background: #dcfce7; color: #15803d; }
|
||||
|
||||
.spot-panel__trend-icon { font-size: 18px; line-height: 1; }
|
||||
.spot-panel__trend-label { font-family: 'Fredoka', sans-serif; }
|
||||
|
||||
/* Details */
|
||||
.spot-panel__details {
|
||||
display: grid;
|
||||
|
||||
@@ -6,6 +6,12 @@ import './SpotPanel.css';
|
||||
const LEVEL_LABEL = { low: 'OK', medium: 'Risque', high: 'Impact' };
|
||||
const TYPE_ICON = { beach: '🏖', port: '⚓', surf: '🏄', fishing: '🎣' };
|
||||
|
||||
const TREND_CONFIG = {
|
||||
increasing: { icon: '📈', label: 'Situation en dégradation', cls: 'trend--bad' },
|
||||
stable: { icon: '➡️', label: 'Situation stable', cls: 'trend--neutral' },
|
||||
decreasing: { icon: '📉', label: 'Amélioration en cours', cls: 'trend--good' },
|
||||
};
|
||||
|
||||
const HORIZON_LABELS = { now: 'Maintenant', h6: '+6h', h12: '+12h', h24: '+24h', h48: '+48h' };
|
||||
|
||||
export default function SpotPanel({ spot, activeStep, onClose }) {
|
||||
@@ -82,13 +88,20 @@ export default function SpotPanel({ spot, activeStep, onClose }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{activeStep === 'now' && displayScore.trend && (() => {
|
||||
const t = TREND_CONFIG[displayScore.trend] ?? TREND_CONFIG.stable;
|
||||
return (
|
||||
<div className={`spot-panel__trend ${t.cls}`}>
|
||||
<span className="spot-panel__trend-icon">{t.icon}</span>
|
||||
<span className="spot-panel__trend-label">{t.label}</span>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
<dl className="spot-panel__details">
|
||||
{displayScore.distance != null && (
|
||||
<><dt>Distance sargasses</dt><dd>{displayScore.distance} km</dd></>
|
||||
)}
|
||||
{activeStep === 'now' && displayScore.trend && (
|
||||
<><dt>Tendance</dt><dd>{displayScore.trend}</dd></>
|
||||
)}
|
||||
{activeStep === 'now' && displayScore.computedAt && (
|
||||
<><dt>Calculé le</dt>
|
||||
<dd>{new Date(displayScore.computedAt).toLocaleString('fr-FR')}</dd></>
|
||||
|
||||
Reference in New Issue
Block a user