Initial commit : docs (specs, data-model, architecture, roadmap)

This commit is contained in:
Gwadaking
2026-03-31 23:24:51 -04:00
commit e44916d444
8 changed files with 1247 additions and 0 deletions

15
.claude/settings.json Normal file
View File

@@ -0,0 +1,15 @@
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "bun ./validate-commands.js"
}
]
}
]
}
}

View File

@@ -0,0 +1,7 @@
{
"permissions": {
"allow": [
"Bash(git commit:*)"
]
}
}

View File

@@ -0,0 +1,426 @@
#!/usr/bin/env bun
/**
* Claude Code "Before Tools" Hook - Command Validation Script
*
* This script validates commands before execution to prevent harmful operations.
* It receives command data via stdin and returns exit code 0 (allow) or 1 (block).
*
* Usage: Called automatically by Claude Code PreToolUse hook
* Manual test: echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /"}}' | bun validate-command.js
*/
// Comprehensive dangerous command patterns database
const SECURITY_RULES = {
// Critical system destruction commands
CRITICAL_COMMANDS: [
"del",
"format",
"mkfs",
"shred",
"dd",
"fdisk",
"parted",
"gparted",
"cfdisk",
],
// Privilege escalation and system access
PRIVILEGE_COMMANDS: [
"sudo",
"su",
"passwd",
"chpasswd",
"usermod",
"chmod",
"chown",
"chgrp",
"setuid",
"setgid",
],
// Network and remote access tools
NETWORK_COMMANDS: [
"nc",
"netcat",
"nmap",
"telnet",
"ssh-keygen",
"iptables",
"ufw",
"firewall-cmd",
"ipfw",
],
// System service and process manipulation
SYSTEM_COMMANDS: [
"systemctl",
"service",
"kill",
"killall",
"pkill",
"mount",
"umount",
"swapon",
"swapoff",
],
// Dangerous regex patterns
DANGEROUS_PATTERNS: [
// File system destruction - block rm -rf with absolute paths
/rm\s+.*-rf\s*\/\s*$/i, // rm -rf ending at root directory
/rm\s+.*-rf\s*\/\w+/i, // rm -rf with any absolute path
/rm\s+.*-rf\s*\/etc/i, // rm -rf in /etc
/rm\s+.*-rf\s*\/usr/i, // rm -rf in /usr
/rm\s+.*-rf\s*\/bin/i, // rm -rf in /bin
/rm\s+.*-rf\s*\/sys/i, // rm -rf in /sys
/rm\s+.*-rf\s*\/proc/i, // rm -rf in /proc
/rm\s+.*-rf\s*\/boot/i, // rm -rf in /boot
/rm\s+.*-rf\s*\/home\/[^\/]*\s*$/i, // rm -rf entire home directory
/rm\s+.*-rf\s*\.\.+\//i, // rm -rf with parent directory traversal
/rm\s+.*-rf\s*\*.*\*/i, // rm -rf with multiple wildcards
/rm\s+.*-rf\s*\$\w+/i, // rm -rf with variables (could be dangerous)
/>\s*\/dev\/(sda|hda|nvme)/i,
/dd\s+.*of=\/dev\//i,
/shred\s+.*\/dev\//i,
/mkfs\.\w+\s+\/dev\//i,
// Fork bomb and resource exhaustion
/:\(\)\{\s*:\|:&\s*\};:/,
/while\s+true\s*;\s*do.*done/i,
/for\s*\(\(\s*;\s*;\s*\)\)/i,
// Command injection and chaining
/;\s*(rm|dd|mkfs|format)/i,
/&&\s*(rm|dd|mkfs|format)/i,
/\|\|\s*(rm|dd|mkfs|format)/i,
// Remote code execution
/\|\s*(sh|bash|zsh|fish)$/i,
/(wget|curl)\s+.*\|\s*(sh|bash)/i,
/(wget|curl)\s+.*-O-.*\|\s*(sh|bash)/i,
// Command substitution with dangerous commands
/`.*rm.*`/i,
/\$\(.*rm.*\)/i,
/`.*dd.*`/i,
/\$\(.*dd.*\)/i,
// Sensitive file access
/cat\s+\/etc\/(passwd|shadow|sudoers)/i,
/>\s*\/etc\/(passwd|shadow|sudoers)/i,
/echo\s+.*>>\s*\/etc\/(passwd|shadow|sudoers)/i,
// Network exfiltration
/\|\s*nc\s+\S+\s+\d+/i,
/curl\s+.*-d.*\$\(/i,
/wget\s+.*--post-data.*\$\(/i,
// Log manipulation
/>\s*\/var\/log\//i,
/rm\s+\/var\/log\//i,
/echo\s+.*>\s*~?\/?\.bash_history/i,
// Backdoor creation
/nc\s+.*-l.*-e/i,
/nc\s+.*-e.*-l/i,
/ncat\s+.*--exec/i,
/ssh-keygen.*authorized_keys/i,
// Crypto mining and malicious downloads
/(wget|curl).*\.(sh|py|pl|exe|bin).*\|\s*(sh|bash|python)/i,
/(xmrig|ccminer|cgminer|bfgminer)/i,
// Hardware direct access
/cat\s+\/dev\/(mem|kmem)/i,
/echo\s+.*>\s*\/dev\/(mem|kmem)/i,
// Kernel module manipulation
/(insmod|rmmod|modprobe)\s+/i,
// Cron job manipulation
/crontab\s+-e/i,
/echo\s+.*>>\s*\/var\/spool\/cron/i,
// Environment variable exposure
/env\s*\|\s*grep.*PASSWORD/i,
/printenv.*PASSWORD/i,
],
// Paths that should never be written to
PROTECTED_PATHS: [
"/etc/",
"/usr/",
"/bin/",
"/sbin/",
"/boot/",
"/sys/",
"/proc/",
"/dev/",
"/root/",
],
};
// Allowlist of safe commands (when used appropriately)
const SAFE_COMMANDS = [
"ls",
"dir",
"pwd",
"whoami",
"date",
"echo",
"cat",
"head",
"tail",
"grep",
"find",
"wc",
"sort",
"uniq",
"cut",
"awk",
"sed",
"git",
"npm",
"pnpm",
"node",
"bun",
"python",
"pip",
"cd",
"cp",
"mv",
"mkdir",
"touch",
"ln",
];
class CommandValidator {
constructor() {
this.logFile = "/Users/melvynx/.claude/security.log";
}
/**
* Main validation function
*/
validate(command, toolName = "Unknown") {
const result = {
isValid: true,
severity: "LOW",
violations: [],
sanitizedCommand: command,
};
if (!command || typeof command !== "string") {
result.isValid = false;
result.violations.push("Invalid command format");
return result;
}
// Normalize command for analysis
const normalizedCmd = command.trim().toLowerCase();
const cmdParts = normalizedCmd.split(/\s+/);
const mainCommand = cmdParts[0];
// Check against critical commands
if (SECURITY_RULES.CRITICAL_COMMANDS.includes(mainCommand)) {
result.isValid = false;
result.severity = "CRITICAL";
result.violations.push(`Critical dangerous command: ${mainCommand}`);
}
// Check privilege escalation commands
if (SECURITY_RULES.PRIVILEGE_COMMANDS.includes(mainCommand)) {
result.isValid = false;
result.severity = "HIGH";
result.violations.push(`Privilege escalation command: ${mainCommand}`);
}
// Check network commands
if (SECURITY_RULES.NETWORK_COMMANDS.includes(mainCommand)) {
result.isValid = false;
result.severity = "HIGH";
result.violations.push(`Network/remote access command: ${mainCommand}`);
}
// Check system commands
if (SECURITY_RULES.SYSTEM_COMMANDS.includes(mainCommand)) {
result.isValid = false;
result.severity = "HIGH";
result.violations.push(`System manipulation command: ${mainCommand}`);
}
// Check dangerous patterns
for (const pattern of SECURITY_RULES.DANGEROUS_PATTERNS) {
if (pattern.test(command)) {
result.isValid = false;
result.severity = "CRITICAL";
result.violations.push(`Dangerous pattern detected: ${pattern.source}`);
}
}
// Check for protected path access (but allow common redirections like /dev/null)
for (const path of SECURITY_RULES.PROTECTED_PATHS) {
if (command.includes(path)) {
// Allow common safe redirections
if (
path === "/dev/" &&
(command.includes("/dev/null") ||
command.includes("/dev/stderr") ||
command.includes("/dev/stdout"))
) {
continue;
}
result.isValid = false;
result.severity = "HIGH";
result.violations.push(`Access to protected path: ${path}`);
}
}
// Additional safety checks
if (command.length > 2000) {
result.isValid = false;
result.severity = "MEDIUM";
result.violations.push("Command too long (potential buffer overflow)");
}
// Check for binary/encoded content
if (/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\xFF]/.test(command)) {
result.isValid = false;
result.severity = "HIGH";
result.violations.push("Binary or encoded content detected");
}
return result;
}
/**
* Log security events
*/
async logSecurityEvent(command, toolName, result, sessionId = null) {
const timestamp = new Date().toISOString();
const logEntry = {
timestamp,
sessionId,
toolName,
command: command.substring(0, 500), // Truncate for logs
blocked: !result.isValid,
severity: result.severity,
violations: result.violations,
source: "claude-code-hook",
};
try {
// Write to log file
const logLine = JSON.stringify(logEntry) + "\n";
await Bun.write(this.logFile, logLine, { createPath: true, flag: "a" });
// Also output to stderr for immediate visibility
console.error(
`[SECURITY] ${
result.isValid ? "ALLOWED" : "BLOCKED"
}: ${command.substring(0, 100)}`,
);
} catch (error) {
console.error("Failed to write security log:", error);
}
}
/**
* Check if command matches any allowed patterns from settings
*/
isExplicitlyAllowed(command, allowedPatterns = []) {
for (const pattern of allowedPatterns) {
// Convert Claude Code permission pattern to regex
// e.g., "Bash(git *)" becomes /^git\s+.*$/
if (pattern.startsWith("Bash(") && pattern.endsWith(")")) {
const cmdPattern = pattern.slice(5, -1); // Remove "Bash(" and ")"
const regex = new RegExp(
"^" + cmdPattern.replace(/\*/g, ".*") + "$",
"i",
);
if (regex.test(command)) {
return true;
}
}
}
return false;
}
}
/**
* Main execution function
*/
async function main() {
const validator = new CommandValidator();
try {
// Read hook input from stdin
const stdin = process.stdin;
const chunks = [];
for await (const chunk of stdin) {
chunks.push(chunk);
}
const input = Buffer.concat(chunks).toString();
if (!input.trim()) {
console.error("No input received from stdin");
process.exit(1);
}
// Parse Claude Code hook JSON format
let hookData;
try {
hookData = JSON.parse(input);
} catch (error) {
console.error("Invalid JSON input:", error.message);
process.exit(1);
}
const toolName = hookData.tool_name || "Unknown";
const toolInput = hookData.tool_input || {};
const sessionId = hookData.session_id || null;
// Only validate Bash commands for now
if (toolName !== "Bash") {
console.log(`Skipping validation for tool: ${toolName}`);
process.exit(0);
}
const command = toolInput.command;
if (!command) {
console.error("No command found in tool input");
process.exit(1);
}
// Validate the command
const result = validator.validate(command, toolName);
// Log the security event
await validator.logSecurityEvent(command, toolName, result, sessionId);
// Output result and exit with appropriate code
if (result.isValid) {
console.log("Command validation passed");
process.exit(0); // Allow execution
} else {
console.error(
`Command validation failed: ${result.violations.join(", ")}`,
);
console.error(`Severity: ${result.severity}`);
process.exit(2); // Block execution (Claude Code requires exit code 2)
}
} catch (error) {
console.error("Validation script error:", error);
// Fail safe - block execution on any script error
process.exit(2);
}
}
// Execute main function
main().catch((error) => {
console.error("Fatal error:", error);
process.exit(2);
});

202
claude.md Normal file
View File

@@ -0,0 +1,202 @@
# 🧠 Claude Project Context — Sargasse-Sentry
## 🎯 Objectif du Projet
Construire une plateforme de renseignement maritime capable de transformer des données satellites complexes en une information simple, fiable et immédiatement exploitable par des utilisateurs non techniques.
Le projet doit privilégier :
- la robustesse
- la lisibilité
- la performance
- la maintenabilité long terme
Toute décision technique doit être orientée vers ces objectifs.
---
## ⚙️ Stack Technique (NON NÉGOCIABLE)
- Backend : Symfony 7.4 LTS (PHP 8.4+)
- Database : PostgreSQL + PostGIS
- Queue : Symfony Messenger + Redis
- Cartographie : Mapbox GL JS
- Cache : Redis + HTTP Cache
- Traitement : PHP prioritaire, Python uniquement si nécessaire
---
## 🧱 Principes dArchitecture
### 1. Séparation stricte des responsabilités
- Observation ≠ Prédiction ≠ Score
- Ne jamais mélanger ces concepts dans une même entité ou table
---
### 2. Aucune logique métier dans les contrôleurs
- Utiliser des services
- Utiliser des handlers Messenger pour les traitements lourds
---
### 3. Pipeline asynchrone obligatoire
- Toute ingestion ou traitement doit passer par Messenger
- Aucun traitement bloquant en requête HTTP
---
### 4. Données immuables
- Une observation ne doit jamais être modifiée
- Une prédiction est versionnée (modelVersion)
---
### 5. Optimisation géospatiale native
- Utiliser PostGIS pour :
- distance
- intersection
- simplification
- Ne jamais recalculer côté PHP si PostGIS peut le faire
---
## 📊 Conventions de Données
### Géométrie
- SRID : 4326 obligatoire
- Utiliser MULTIPOLYGON même pour un seul polygone
- Simplifier avant stockage
---
### Dates
- Toujours en UTC
- Utiliser DateTimeImmutable
---
### Identifiants
- UUID pour toutes les entités critiques
---
## 🧠 Modélisation
Claude DOIT créer des entités distinctes :
- SargassumObservation
- SargassumForecast
- CoastalPoint
- ImpactScore
- DataIngestionJob
Aucune fusion ou simplification nest autorisée.
---
## 🌊 Pipeline
Claude DOIT implémenter :
1. Ingestion Sentinel
2. Calcul AFAI
3. Vectorisation
4. Simulation de dérive par points
5. Génération de prédictions multiples
---
## ⚠️ Contraintes critiques
### 1. Performance
- Interdiction denvoyer du GeoJSON brut en production
- Utilisation obligatoire de vector tiles
---
### 2. Cache
- Toute donnée calculée doit être cachée
- Aucun recalcul inutile
---
### 3. Résilience
- Retry automatique sur ingestion
- Gestion des erreurs obligatoire
---
## 🎯 UX Constraints
Claude doit considérer que :
- Lutilisateur ne comprend pas les données satellites
- Linformation doit être lisible en moins de 3 secondes
Donc :
- Toujours fournir un score simple
- Toujours fournir une tendance
- Toujours fournir un horizon temporel
---
## 🔁 Évolutivité
Le système doit être conçu pour :
- ajouter de nouvelles sources de données
- améliorer le modèle de dérive
- intégrer du machine learning
Sans refonte complète.
---
## 🚫 Interdictions
- Pas de logique métier dans les contrôleurs
- Pas de calcul géospatial lourd en PHP si PostGIS peut le faire
- Pas de dépendance inutile
- Pas de complexité prématurée (microservices non nécessaires au MVP)
---
## ✅ Attentes vis-à-vis de Claude
Claude doit :
- générer du code propre, structuré, testable
- respecter strictement les conventions
- documenter les choix techniques
- proposer des améliorations si pertinentes
Claude ne doit PAS :
- simplifier les modèles de données
- ignorer les contraintes de performance
- court-circuiter Messenger
---
## 🧭 Philosophie
Ce projet nest pas une simple application cartographique.
Cest un système de renseignement environnemental.
Chaque décision doit renforcer :
- la précision
- la fiabilité
- la compréhension utilisateur

150
docs/architecture.md Normal file
View File

@@ -0,0 +1,150 @@
# ARCHITECTURE — Sargasse-Sentry
## Stack technique
| Couche | Technologie | Rôle |
|---|---|---|
| Backend | Symfony (PHP) | API JSON, crons, pipeline |
| Frontend | React SPA | Interface utilisateur |
| Maps | MapLibre GL JS + react-map-gl | Rendu cartographique |
| Base de données | PostgreSQL + PostGIS | Données géospatiales |
| Vector tiles | Tippecanoe (génération) | Tiles statiques pré-générées |
| Cache | Redis | Scores, résultats API |
| Reverse proxy | Traefik | Routage multi-tenant, SSL |
| Runtime PHP | FrankenPHP | Remplace php-fpm + nginx |
| Conteneurisation | Docker | Isolation, déploiement |
---
## Infrastructure
### Hébergement
VPS personnel, architecture **multi-tenant Docker + Traefik + FrankenPHP** (configuration existante partagée entre projets).
```
Internet
└── Traefik (reverse proxy, SSL Let's Encrypt)
├── sargasse-sentry.tld → container Symfony/FrankenPHP
├── [autres projets]
└── ...
Containers Sargasse-Sentry :
├── frankenphp (Symfony API + worker cron)
├── postgres (PostgreSQL + PostGIS)
└── redis (cache)
```
### CDN (optionnel, sans coût)
**Cloudflare free tier** peut être activé devant Traefik pour mettre en cache les vector tiles statiques et les réponses HTTP publiques. Aucun coût.
---
## Architecture applicative
### Backend — Symfony API
Symfony n'expose que du JSON (pas de Twig). Deux responsabilités :
1. **API REST** — endpoints consommés par le frontend React
2. **Pipeline** — crons + workers de traitement satellite
### Frontend — React SPA
Application React statique servie par FrankenPHP (ou Traefik directement). Consomme l'API Symfony et affiche les vector tiles via MapLibre GL JS.
---
## API Endpoints
### Spots & scores
```
GET /api/spots Liste des CoastalPoints
GET /api/spots/{id} Détail d'un CoastalPoint
GET /api/spots/{id}/score ImpactScore courant + horizons
GET /api/spots/{id}/score?at={datetime} Score à un instant donné
```
### Observations & prédictions
```
GET /api/observations?bbox={bbox}&date={date} Observations dans une zone
GET /api/observations/{id} Détail observation
GET /api/forecasts?observationId={id} Prédictions d'une observation
GET /api/forecasts?bbox={bbox}&horizon={h} Prédictions par zone + horizon
```
### Feedback
```
POST /api/feedback Soumettre un UserFeedback anonyme
```
### Tiles (statiques, hors API)
```
GET /tiles/observations/{z}/{x}/{y}.pbf
GET /tiles/forecasts/{horizon}/{z}/{x}/{y}.pbf
```
---
## Vector Tiles
### Stratégie : Tippecanoe + tiles statiques
- Les observations et prédictions sont converties en tiles `.pbf` après chaque ingestion (via Tippecanoe)
- Les tiles sont stockées sur le filesystem du VPS et servies statiquement
- Pas de rendu dynamique à la volée (Tegola écarté — complexité non justifiée au MVP)
- Cloudflare free tier peut mettre ces tiles en cache si activé
### Interdits
- GeoJSON brut exposé en production sur des géométries complexes
---
## Caching
| Données | Stratégie | TTL suggéré |
|---|---|---|
| Scores par spot | Redis | 3h (durée du cycle d'ingestion) |
| Résultats API observations | Redis | 3h |
| Vector tiles statiques | HTTP Cache + Cloudflare | Long (invalidation à chaque ingestion) |
| Endpoints publics | HTTP Cache | 1530 min |
Règle : **1 requête = 1 zone + 1 timestamp**, pas de recalcul à la volée.
---
## Sécurité
- Rate limiting sur tous les endpoints publics (Symfony RateLimiter)
- Aucune donnée personnelle collectée (UserFeedback = fingerprint anonyme)
- Retry automatique pipeline avec backoff
- Logs structurés sur chaque DataIngestionJob
- Secrets (clés API Sentinel Hub, NOAA) via variables d'environnement Docker
---
## Schéma de flux de données
```
Sentinel Hub API
└── Cron Symfony (3h)
└── DataIngestionJob
├── Calcul AFAI → SargassumObservation
│ └── Tippecanoe → tiles statiques /tiles/observations/
└── Simulation dérive → SargassumForecast (×4 horizons)
├── Calcul ImpactScore par CoastalPoint → Redis
└── Tippecanoe → tiles statiques /tiles/forecasts/
NOAA GRIB API
└── (consommé pendant la simulation de dérive)
React SPA
├── MapLibre GL JS → /tiles/...pbf
└── Symfony API → /api/spots/{id}/score
```

141
docs/data-model.md Normal file
View File

@@ -0,0 +1,141 @@
# DATA MODEL — Sargasse-Sentry
## Principe fondamental
Séparer strictement :
- **Observation** — ce qui a été détecté (réel)
- **Forecast** — ce qui est simulé (prédiction)
- **Job** — traçabilité du pipeline
- **Score** — agrégat calculé pour un point côtier
- **Feedback** — retour terrain anonyme
---
## Entité : SargassumObservation
Représente une détection satellite réelle.
| Champ | Type | Notes |
|---|---|---|
| `id` | UUID | PK |
| `geometry` | MULTIPOLYGON (SRID 4326) | Polygones détectés |
| `bbox` | GEOMETRY BOX | Bounding box — requêtes spatiales légères |
| `detectedAt` | datetime UTC | Immutable |
| `source` | string | `Sentinel-2` / `Sentinel-3` |
| `tileId` | string | Identifiant tuile Sentinel |
| `cloudCoverage` | float 0100 | % couverture nuageuse |
| `confidence` | float 01 | Indice de confiance détection |
| `afaiMean` | float | Valeur AFAI moyenne |
| `afaiStd` | float | Écart-type AFAI |
| `coverageArea` | float (km²) | Surface totale détectée |
| `processingVersion` | string | Version de l'algorithme AFAI utilisé |
| `createdAt` | datetime | |
**Index :**
- `GIST (geometry)`
- `GIST (bbox)`
- `BTREE (detectedAt)`
- `BTREE (source)`
---
## Entité : SargassumForecast
Projection issue du modèle de dérive.
| Champ | Type | Notes |
|---|---|---|
| `id` | UUID | PK |
| `sourceObservation` | FK → SargassumObservation | |
| `geometry` | MULTIPOLYGON (SRID 4326) | Polygone prédit |
| `validAt` | datetime UTC | Moment auquel la prédiction est valide |
| `computedAt` | datetime | Moment du calcul |
| `modelVersion` | string | Version du modèle de dérive |
| `timeHorizon` | int (heures) | `6 / 12 / 24 / 48` |
| `confidence` | float 01 | |
| `driftVectorAvg` | GEOMETRY(POINT, 4326) | Vecteur de dérive moyen (PostGIS natif) |
| `createdAt` | datetime | |
**Index :**
- `GIST (geometry)`
- `BTREE (validAt)`
- `BTREE (timeHorizon)`
---
## Entité : DataIngestionJob
Traçabilité du pipeline — robustesse obligatoire.
| Champ | Type | Notes |
|---|---|---|
| `id` | UUID | PK |
| `source` | string | `Sentinel-2` / `Sentinel-3` |
| `tileId` | string | |
| `requestedAt` | datetime | |
| `processedAt` | datetime (nullable) | |
| `status` | enum | `pending / success / failed` |
| `retryCount` | int | |
| `errorMessage` | string (nullable) | |
---
## Entité : CoastalPoint
Points d'intérêt utilisateur.
| Champ | Type | Notes |
|---|---|---|
| `id` | UUID | PK |
| `name` | string | |
| `geometry` | POINT (SRID 4326) | |
| `type` | enum | `beach / port / surf / fishing` |
| `region` | string | |
---
## Entité : ImpactScore
Score calculé pour un point côtier à un instant donné.
| Champ | Type | Notes |
|---|---|---|
| `id` | UUID | PK |
| `coastalPoint` | FK → CoastalPoint | |
| `timestamp` | datetime | |
| `score` | int 0100 | |
| `level` | enum | `low / medium / high` |
| `distanceToNearestSargassum` | float (km) | |
| `densityEstimate` | float | |
| `trend` | enum | `increasing / stable / decreasing` |
---
## Entité : UserFeedback
Retour terrain anonyme — alimente la boucle d'apprentissage.
| Champ | Type | Notes |
|---|---|---|
| `id` | UUID | PK |
| `location` | POINT (SRID 4326) | Localisation signalée |
| `coastalPoint` | FK → CoastalPoint (nullable) | Si associé à un point connu |
| `timestamp` | datetime | |
| `hasSeaweed` | bool | Présence confirmée ou infirmée |
| `density` | enum (nullable) | `low / medium / high` |
| `source` | string | Fingerprint anonyme (IP hachée) — pas de compte |
---
## Schéma des relations
```
SargassumObservation
└── SargassumForecast (N)
CoastalPoint
└── ImpactScore (N)
└── UserFeedback (N, nullable)
DataIngestionJob (indépendant, audit trail)
```

136
docs/roadmap.md Normal file
View File

@@ -0,0 +1,136 @@
# ROADMAP — Sargasse-Sentry
> Checklist permanente d'avancement projet.
> Mettre à jour au fil des implémentations.
---
## Légende
- [ ] À faire
- [~] En cours
- [x] Terminé
---
## Phase 0 — Fondations
### Infrastructure & environnement
- [ ] Initialisation dépôt Git
- [ ] Configuration Docker (FrankenPHP + PostgreSQL/PostGIS + Redis)
- [ ] Configuration Traefik (domaine, SSL)
- [ ] Variables d'environnement (Sentinel Hub API key, NOAA, DB, Redis)
- [ ] Initialisation projet Symfony
- [ ] Installation API Platform (ou controllers JSON manuels)
- [ ] Initialisation projet React
- [ ] Configuration MapLibre GL JS + react-map-gl
### Base de données
- [ ] Extension PostGIS activée
- [ ] Migration : `SargassumObservation`
- [ ] Migration : `SargassumForecast`
- [ ] Migration : `DataIngestionJob`
- [ ] Migration : `CoastalPoint`
- [ ] Migration : `ImpactScore`
- [ ] Migration : `UserFeedback`
- [ ] Index spatiaux (GIST) + index BTREE définis
---
## Phase 1 — MVP (Antilles, observations uniquement)
### Pipeline d'ingestion
- [ ] Cron Symfony toutes les 3h
- [ ] Appel API Sentinel Hub (bandes B04, B08, B11)
- [ ] Création `DataIngestionJob` (pending → success/failed)
- [ ] Retry automatique + logging erreurs
- [ ] Filtrage nuages (seuil configurable, défaut 60%)
- [ ] Calcul AFAI (formule complète avec ratio longueurs d'onde)
- [ ] Rasterisation → raster binaire sargasse/non
- [ ] Vectorisation (raster → polygones, Douglas-Peucker, suppression bruit)
- [ ] Persistance `SargassumObservation`
- [ ] Génération vector tiles (Tippecanoe → `.pbf`)
### API Backend
- [ ] `GET /api/spots` — liste CoastalPoints
- [ ] `GET /api/spots/{id}` — détail
- [ ] `GET /api/observations?bbox=&date=` — observations par zone
### Frontend React
- [ ] Carte de base (MapLibre GL JS)
- [ ] Affichage vector tiles observations
- [ ] Sélection d'un CoastalPoint (Spot Mode)
- [ ] Affichage polygones observés
### Données initiales
- [ ] Seed `CoastalPoint` zone Antilles (plages, ports, spots surf, zones pêche)
---
## Phase 2 — Dérive & Score
### Pipeline — simulation de dérive
- [ ] Accès NOAA GRIB (vent + courant)
- [ ] Échantillonnage polygone (centroids + random sampling)
- [ ] Application vecteur dérive par point
- [ ] Reconstruction polygone (convex hull / alpha shape)
- [ ] Génération horizons H+6, H+12, H+24, H+48
- [ ] Persistance `SargassumForecast`
- [ ] Génération vector tiles prédictions (Tippecanoe)
### Calcul ImpactScore
- [ ] Calcul score par `CoastalPoint` (distance, densité, vitesse d'approche, tendance)
- [ ] Persistance `ImpactScore`
- [ ] Mise en cache Redis (TTL 3h)
### API Backend
- [ ] `GET /api/spots/{id}/score` — score courant + horizons
- [ ] `GET /api/spots/{id}/score?at={datetime}` — score à un instant
- [ ] `GET /api/forecasts?observationId=` — prédictions par observation
- [ ] `GET /api/forecasts?bbox=&horizon=` — prédictions par zone + horizon
### Frontend React
- [ ] Spot Mode complet (OK / Risque / Impact par horizon)
- [ ] Slider temporel (Now → +6h → +12h → +24h → +48h)
- [ ] Mise à jour dynamique polygones + score sur slider
- [ ] Gradients radiaux densité
- [ ] Animation légère sur polygones de prédiction
- [ ] Vue mobile optimisée
---
## Phase 3 — Feedback & Alertes
### Boucle d'apprentissage
- [ ] Bouton "Je confirme présence de sargasses"
- [ ] `POST /api/feedback` — persistance `UserFeedback` anonyme
- [ ] Intégration feedback dans calcul score (pondération)
### Alertes push
- [ ] Définir modalité d'inscription légère (push token navigateur ou email)
- [ ] Système d'abonnement par `CoastalPoint`
- [ ] Worker Symfony — envoi alertes si score dépasse seuil
- [ ] Interface d'inscription (friction minimale)
---
## Transversal (continu)
- [ ] Rate limiting endpoints publics
- [ ] Monitoring pipeline (alertes si ingestion échoue > N fois)
- [ ] Logs structurés
- [ ] Fallback Sentinel-2 → Sentinel-3
- [ ] Configuration Cloudflare free tier (cache tiles statiques)
- [ ] Tests fonctionnels pipeline (ingestion → score)

170
docs/specs.md Normal file
View File

@@ -0,0 +1,170 @@
# SPECS — Sargasse-Sentry
## Vision Produit
Sargasse-Sentry est une plateforme de renseignement maritime autonome transformant des données satellites brutes en information directement exploitable pour la prise de décision terrain.
Le produit ne doit pas être perçu comme une carte, mais comme un **outil d'aide à la décision en environnement incertain**, avec un focus sur :
- anticipation
- lisibilité immédiate
- confiance dans la donnée
### Utilisateurs cibles
| Profil | Usage |
|---|---|
| Marins-pêcheurs | Décision de sortie |
| Surfeurs | Qualité du spot |
| Baigneurs | Sécurité / nuisance |
### Contraintes produit
- Gratuit
- Sans inscription obligatoire (sauf alertes push, voir Roadmap V3)
- Mobile-first
- Compréhensible en moins de 3 secondes
---
## UX — Logique Produit
### Mode principal : Spot Mode
L'utilisateur sélectionne un point côtier → affichage :
| Horizon | Indicateur |
|---|---|
| Aujourd'hui | OK / Risque / Impact |
| +24h | OK / Risque / Impact |
| +48h | OK / Risque / Impact |
### Timeline interactive
Slider temporel : `Now → +6h → +12h → +24h → +48h`
Met à jour en temps réel :
- les polygones sur la carte
- le score d'impact
### Impact Score
Calcul basé sur :
```
score = f(distance côte, densité, vitesse d'approche, évolution tendancielle)
```
| Score | Niveau |
|---|---|
| 030 | OK |
| 3070 | Risque |
| 70100 | Impact |
### Visualisation
- Gradients radiaux pour la densité
- Contour épais pour la lisibilité
- Animation légère **uniquement** sur les prédictions (pas les observations)
### Boucle d'apprentissage (différenciateur)
Bouton : **"Je confirme présence de sargasses"**
Stocke un `UserFeedback` anonyme (localisation, timestamp, confirmation, densité estimée).
---
## Pipeline de Données
### Étape 1 — Ingestion
- Cron Symfony toutes les 3h
- Appel API Sentinel Hub
- Récupération bandes B04 (RED), B08 (NIR), B11 (SWIR1)
- Création d'un `DataIngestionJob`
### Étape 2 — Pré-traitement
- Filtrage nuages : rejet si `cloudCoverage > seuil`
- Seuil configurable par région (défaut 60% — peut être ajusté pour zones tropicales à forte nébulosité)
### Étape 3 — Calcul AFAI
Formule complète :
```
AFAI = R_NIR - R_RED - (R_SWIR1 - R_RED) × (λ_NIR - λ_RED) / (λ_SWIR1 - λ_RED)
```
- `R_*` : réflectance de surface des bandes Sentinel
- `λ_*` : longueurs d'onde centrales (constantes fixes par capteur)
Résultat : raster binaire (sargasse / non-sargasse)
### Étape 4 — Vectorisation
- Raster → polygones
- Simplification Douglas-Peucker
- Suppression du bruit (polygones < seuil surface minimal)
### Étape 5 — Simulation de dérive (CRITIQUE)
Ne pas appliquer de translation globale.
Algorithme :
1. **Échantillonnage** du polygone : génération de points internes (centroids + random sampling)
2. **Pour chaque point** : récupération vent + courant (GRIB NOAA) → application du vecteur de dérive
3. **Reconstruction** du polygone : convex hull ou alpha shape
4. **Génération des horizons** : H+6, H+12, H+24, H+48
### Sources de données
| Source | Usage | Notes |
|---|---|---|
| Sentinel-2 | Observations haute résolution | Faible fréquence |
| Sentinel-3 | Fallback | Fréquence élevée |
| NOAA GRIB | Vent + courant | Gratuit, accès HTTP direct |
---
## Performance
### Caching
- **Redis** : résultats API, scores d'impact
- **HTTP Cache** : endpoints publics, tiles statiques
- **Cloudflare free tier** (optionnel, sans coût) : CDN pour les vector tiles statiques
### Stratégie
- 1 requête = 1 zone + 1 timestamp
- Pas de recalcul à la volée
- Scores pré-calculés à chaque ingestion
---
## Sécurité & Résilience
- Retry automatique sur ingestion échouée
- Fallback Sentinel-2 → Sentinel-3 si indisponible
- Logs détaillés sur chaque `DataIngestionJob`
- Monitoring des erreurs pipeline
- Rate limiting sur les endpoints publics
---
## Roadmap
### MVP
- Ingestion Sentinel-2
- Calcul AFAI + vectorisation
- Affichage polygones (observations uniquement)
- Zone géographique : Antilles
### V2
- Dérive H+6 / H+12 / H+24 / H+48
- Score d'impact par `CoastalPoint`
- Interface Spot Mode complète
### V3
- Alertes push (nécessite inscription légère : push token ou email — à concevoir en minimisant la friction)
- Boucle feedback utilisateur (`UserFeedback`)
- Amélioration du modèle de dérive via données terrain