diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 000000000..c14692f63 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,36 @@ +name: Deploy +on: + push: + branches: [ main ] + +jobs: + deploy: + runs-on: ubuntu-latest + environment: production + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20.11' + cache: 'npm' + cache-dependency-path: 'react-app/package-lock.json' + - name: Build (react-app) + working-directory: react-app + run: | + npm ci + npm run build + + - name: Add SSH key + uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: | + ${{ secrets.SSH_PRIVATE_KEY }} + + - name: Sync build to server & reload nginx + env: + SSH_HOST: ${{ secrets.SSH_HOST }} + SSH_USER: ${{ secrets.SSH_USER }} + SSH_PATH: ${{ secrets.SSH_PATH }} + run: | + rsync -avz --delete -e "ssh -o StrictHostKeyChecking=no" react-app/build/ ${SSH_USER}@${SSH_HOST}:${SSH_PATH}/ + ssh -o StrictHostKeyChecking=no ${SSH_USER}@${SSH_HOST} "sudo nginx -t && sudo systemctl reload nginx" diff --git a/docs/FISHERMEN_API_FULL.md b/docs/FISHERMEN_API_FULL.md new file mode 100644 index 000000000..7e569b76c --- /dev/null +++ b/docs/FISHERMEN_API_FULL.md @@ -0,0 +1,58 @@ +# 🌊 API Pescadores - Integración Completa (Geo + Pronóstico + Forense) + +## Endpoints (Backend) + +- GET `/api/fishermen/location?latitude=&longitude=` + - Devuelve info de geolocalización usando `modules.geo.get_location` +- POST `/api/fishermen/weather` + - Body: `{ latitude, longitude, include_fishing_recommendations }` + - Usa `FishermenApp.get_weather` y agrega `fishing_tips` +- GET `/api/fishermen/complete?latitude=&longitude=` + - Devuelve `get_geo_info` con clima, mapas y ubicación + extra para pescadores +- GET `/api/fishermen/status` + - Estado de integraciones (OpenWeather, Google Maps, IBGE, local) + +Fuente principal: `backend/iuri_server_dark.py` (sección API PESCADORES) + +## Frontend React + +Servicio: `react-app/src/services/fishermenGeo.ts` +- `getLocation(lat, lon)` +- `getWeather({ latitude, longitude, include_fishing_recommendations })` +- `getComplete(lat, lon)` +- `getStatus()` + +UI: `react-app/src/components/FishermenDashboard.tsx` (listado) y App base (`react-app/src/App.tsx`). + +## Integración Geo + +- Módulo: `modules/geo` (location, coordinates, weather, mapas) +- Función integrada: `get_geo_info(lat, lon)` +- Archivos de referencia: + - `backend/fishermen_geo_api.py` + - `docs/GEO_FISHERMEN_INTEGRATION.md` + +## Zonas óptimas y previsión (base) + +- Config especies y zonas: `configs/offshore_species.yaml` +- Cálculo hotspots (scoring): `scripts/fetch_offshore.py` +- Datos oceánicos/marinos demo: `modules/oceanus/service.py` + +Siguiente paso sugerido (PR futuro): +- Exponer `GET /api/fishermen/hotspots?lat=&lon=&species=` que consuma `fetch_offshore.py` o un servicio ligero para puntuar zonas. + +## Forense y auditoría + +- Guardian de logs: `backend/log_guardian.py` +- Config de triaje y auditorías: `configs/triage_config.yaml`, `scripts/triage.py` +- Propuesta: Loggear cada request `fishermen_*` con `log_guardian.log_critical_action` y auditar periódicamente. + +## Comandos de verificación + +- Backend local: `uvicorn backend.iuri_server_dark:app --reload` +- Frontend: `cd react-app && npm ci && npm run build && npm test -- --watch=false --passWithNoTests` + +## CI/CD + +- CI: `.github/workflows/ci.yml` (ejecuta en `react-app/`) +- Deploy: `.github/workflows/deploy.yml` + scripts Hetzner diff --git a/react-app/src/services/fishermenGeo.ts b/react-app/src/services/fishermenGeo.ts new file mode 100644 index 000000000..23eceba74 --- /dev/null +++ b/react-app/src/services/fishermenGeo.ts @@ -0,0 +1,42 @@ +export interface Coordinates { + latitude: number; + longitude: number; +} + +export interface WeatherRequest extends Coordinates { + include_fishing_recommendations?: boolean; +} + +export async function getLocation(latitude: number, longitude: number, base = '/api/fishermen') { + const url = `${base}/location?latitude=${encodeURIComponent(latitude)}&longitude=${encodeURIComponent(longitude)}`; + const res = await fetch(url); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + return res.json(); +} + +export async function getWeather(body: WeatherRequest, base = '/api/fishermen') { + const url = `${base}/weather`; + const res = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + return res.json(); +} + +export async function getComplete(latitude: number, longitude: number, base = '/api/fishermen') { + const url = `${base}/complete?latitude=${encodeURIComponent(latitude)}&longitude=${encodeURIComponent(longitude)}`; + const res = await fetch(url); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + return res.json(); +} + +export async function getStatus(base = '/api/fishermen') { + const url = `${base}/status`; + const res = await fetch(url); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + return res.json(); +} + + diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 000000000..1c86bc9c5 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,30 @@ +import React, { useState } from 'react' +import MemoryConsole from './components/MemoryConsole' +import FishermenDashboard from './components/FishermenDashboard' + +function App() { + const [view, setView] = useState<'home'|'memory'|'fishermen'>('home') + return ( +
+
+

iURi React App

+
+ + + +
+
+ {view==='home' && ( +

Build limpio y verificable

+ )} + {view==='memory' && ( + + )} + {view==='fishermen' && ( + + )} +
+ ) +} + +export default App diff --git a/src/components/FishermenDashboard.tsx b/src/components/FishermenDashboard.tsx new file mode 100644 index 000000000..6e2d2c19b --- /dev/null +++ b/src/components/FishermenDashboard.tsx @@ -0,0 +1,58 @@ +import React, { useState } from 'react'; +import { getLocation, getWeather, getComplete } from '../services/fishermen'; + +export default function FishermenDashboard() { + const [lat, setLat] = useState(-22.9194); + const [lon, setLon] = useState(-42.8186); + const [loading, setLoading] = useState(false); + const [out, setOut] = useState(''); + const [error, setError] = useState(null); + + async function run(fn: () => Promise) { + setLoading(true); + setError(null); + setOut(''); + try { + const data = await fn(); + setOut(JSON.stringify(data, null, 2)); + } catch (e: any) { + setError(e?.message ?? 'Error'); + } finally { + setLoading(false); + } + } + + return ( +
+

🌊 Pescadores

+
+
+
+ + setLat(parseFloat(e.target.value))} style={{width:'100%', background:'#0d1117', color:'#c9d1d9', border:'1px solid #30363d', borderRadius:4, padding:8}} /> +
+
+ + setLon(parseFloat(e.target.value))} style={{width:'100%', background:'#0d1117', color:'#c9d1d9', border:'1px solid #30363d', borderRadius:4, padding:8}} /> +
+
+ + + +
+
+
+

Resultado

+ {loading &&
🔄 Cargando…
} + {error &&
❌ {error}
} + {out && ( +
{out}
+ )} + {!loading && !error && !out &&
} +
+
+
+ ); +} + + diff --git a/src/components/MemoryConsole.tsx b/src/components/MemoryConsole.tsx new file mode 100644 index 000000000..074741792 --- /dev/null +++ b/src/components/MemoryConsole.tsx @@ -0,0 +1,132 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { appendEntry, fetchEntries, fetchStats, type MemoryEntry } from '../services/memory'; + +export default function MemoryConsole() { + const [query, setQuery] = useState(''); + const [limit, setLimit] = useState(100); + const [loading, setLoading] = useState(false); + const [entries, setEntries] = useState([]); + const [stats, setStats] = useState | null>(null); + const [text, setText] = useState(''); + const [tags, setTags] = useState(''); + const [error, setError] = useState(null); + + const parsedTags = useMemo(() => ( + tags + .split(',') + .map(s => s.trim()) + .filter(Boolean) + ), [tags]); + + async function load() { + setLoading(true); + setError(null); + try { + const [e, s] = await Promise.all([ + fetchEntries(query, limit), + fetchStats().catch(() => null), + ]); + setEntries(e); + setStats(s as Record | null); + } catch (err: any) { + setError(err?.message ?? 'Error cargando memoria'); + } finally { + setLoading(false); + } + } + + async function onAppend() { + if (!text.trim()) return; + setLoading(true); + setError(null); + try { + await appendEntry(text.trim(), parsedTags); + setText(''); + setTags(''); + await load(); + } catch (err: any) { + setError(err?.message ?? 'Error agregando entrada'); + } finally { + setLoading(false); + } + } + + useEffect(() => { + load(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( +
+

🧠 Memoria iURi

+ +
+
+

Agregar nota

+