Initial commit : docs (specs, data-model, architecture, roadmap)
This commit is contained in:
15
.claude/settings.json
Normal file
15
.claude/settings.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"hooks": {
|
||||||
|
"PreToolUse": [
|
||||||
|
{
|
||||||
|
"matcher": "Bash",
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "bun ./validate-commands.js"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
7
.claude/settings.local.json
Normal file
7
.claude/settings.local.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(git commit:*)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
426
.claude/validate-commands.js
Normal file
426
.claude/validate-commands.js
Normal 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
202
claude.md
Normal 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 d’Architecture
|
||||||
|
|
||||||
|
### 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 n’est 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 d’envoyer 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 :
|
||||||
|
|
||||||
|
- L’utilisateur ne comprend pas les données satellites
|
||||||
|
- L’information 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 n’est pas une simple application cartographique.
|
||||||
|
|
||||||
|
C’est 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
150
docs/architecture.md
Normal 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 | 15–30 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
141
docs/data-model.md
Normal 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 0–100 | % couverture nuageuse |
|
||||||
|
| `confidence` | float 0–1 | 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 0–1 | |
|
||||||
|
| `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 0–100 | |
|
||||||
|
| `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
136
docs/roadmap.md
Normal 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
170
docs/specs.md
Normal 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 |
|
||||||
|
|---|---|
|
||||||
|
| 0–30 | OK |
|
||||||
|
| 30–70 | Risque |
|
||||||
|
| 70–100 | 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
|
||||||
Reference in New Issue
Block a user