# Handover — MSC Tracker > **Para:** Codex (o el siguiente agente que tome el proyecto) > **De:** Pipo (el agente anterior) > **Fecha:** 2026-06-07 > **Operador humano:** Ignacio (Chicho) — maneja el server, decide el producto, le pasa instrucciones > **Andres:** usuario beta (amigo del operador) — va a integrar el endpoint `/track/{container}` desde su cron PHP --- ## TL;DR Servicio de tracking de containers de MSC expuesto como API. Un worker (Camoufox) scrapea la página de MSC con anti-detect browser, el API lo wrappea con auth multi-usuario y lo expone por Tailscale Funnel. Andres (cron PHP en otra VPS) consume el endpoint, Ignacio lo maneja todo desde un panel web. **Stack:** FastAPI + SQLite + bcrypt · Camoufox worker · Tailscale Funnel · Frontend server-rendered con un toque de vanilla JS **URL pública:** `https://miopenclaw-vnic.tail9799d2.ts.net/msc-api/` --- ## Cómo llegamos hasta acá 1. **v1 (2024–inicio 2026):** worker + API chiquito con auth por shared password. Andres ya lo consumía desde su cron PHP en `http://3.236.179.157:3000/track/{container}`. 2. **v2 (esta sesión, ~junio 2026):** - Migré auth a username+password con SQLite + bcrypt - Agregué roles (admin/user) con `bootstrap_password` desde config - Reemplacé el frontend single-page por páginas separadas con sticky topnav - Agregué help center público con código PHP listo para copiar (`/docs/andres`) - Agregué `llms.txt` + `llms-full.txt` para AI crawlers - Agregué drop-downs en el nav, alineación, paths absolutos (porque Tailscale Funnel agrega `/msc-api/` y rompía paths relativos) - Último: dropdown del Panel con anchor links a secciones **El sistema está corriendo, deployado, y funcional.** Lo único que falta probar end-to-end es la integración real con el cron de Andres (yo no pude testear su PHP code directamente). --- ## Qué tenemos funcionando ### Servicios systemd activos - `api.service` (puerto 9091) — FastAPI backend - `worker.service` (puerto 9090) — Camoufox scraper Ambos corren como user `rastreador`. Logs con `sudo journalctl -u api.service` o `sudo journalctl -u worker.service`. ### Tailscale Funnel - `https://miopenclaw-vnic.tail9799d2.ts.net/msc-api/*` → `127.0.0.1:9091` - Puerto 443 root va a OpenClaw (no tocar) ### Auth (3 métodos equivalentes) 1. `X-Api-Key: usuario:password` (header) 2. `Authorization: Basic ` 3. Cookie `msc_session` (de web login, 7 días) ### Endpoints principales **Públicos (no auth):** - `GET /login` — login form - `GET /docs`, `/docs/{start,andres,api,errors,ops}` — help center - `GET /llms.txt`, `/llms-full.txt` — para AI crawlers - `POST /api/login` — login (body: `{username, password}`) - `GET /api/magic-login?user=admin` — magic login (necesita la key, **debe ir en el header `X-Magic-Key`**, no en URL ni query) **Auth (cualquier user logueado):** - `GET /api/me` — info del user - `POST /api/me/password` — cambiar propio password - `GET /api/containers` — lista de containers trackeados - `POST /api/containers` — agregar (body: `{container, interval_minutes}`) - `DELETE /api/containers/{c}` — borrar - `PATCH /api/containers/{c}` — cambiar intervalo - `POST /api/containers/{c}/track` — forzar track ahora - `POST /api/track` body `{container}` — track one-shot sin guardar - `GET /track/{container}` — **endpoint de Andres**, devuelve `{json: {...}, screenshot: "base64 PNG", screenshot_format: "png"}` (≈1.3MB total) - `GET /api/worker/status`, `GET /api/worker/logs?n=50` **Admin only:** - `GET /api/admin/users` — listar - `POST /api/admin/users` — crear (body: `{username, password, role}`) - `DELETE /api/admin/users/{u}` — borrar - `POST /api/admin/users/{u}/reset-password` — reset - `GET /users` — página web de admin de users **Páginas web (con topnav):** - `/` — panel principal (containers, track rápido, worker, admin, API examples, ayuda) - `/login` — form - `/docs` y sub-páginas - `/users` (admin only) ### Scheduler (background) Loop en `app.py` (`scheduler_loop`) que cada `SCHEDULER_TICK_SECONDS` (60s) revisa containers y dispara `call_worker_track()` para los que estén ready (interval cumplido). Actualiza `state.json`. ### Container ID format 4–20 chars, uppercase alphanumeric. Validado en Pydantic schema. --- ## Dónde está cada cosa ``` /home/rastreador/ ├── README.md ← guía rápida para el user "rastreador" (SSH) ├── HANDOVER.md ← este archivo ├── api/ │ ├── app.py ← FastAPI: routes, auth, scheduler, render HTML (~600 lines) │ ├── config.json ← port, worker_url, magic_key, bootstrap_password, base_path │ ├── users.db ← SQLite (users, sessions, magic_tokens) │ ├── state.json ← lista de containers + last_poll metadata │ ├── static/ │ │ ├── style.css ← CSS compartido (topnav, dropdowns, cards, code, modals) │ │ ├── app.js ← JS compartido (toast, copyCode, changePw, logout, BASE prefix) │ │ ├── login.html ← form de login │ │ ├── index.html ← panel principal (megasection con ayuda embebida) │ │ ├── users.html ← admin user management │ │ └── doc/ │ │ ├── start.html ← "Para empezar" │ │ ├── andres.html ← "Para Andres (cron PHP)" — código listo │ │ ├── api.html ← Referencia de API │ │ ├── errors.html ← Troubleshooting │ │ └── ops.html ← Estructura y operación │ └── app.py.bak-v2-users ← backup de la v1 (por si hay que volver) ├── worker/ │ ├── worker.py ← Camoufox scraper │ ├── venv/ ← Python venv con camoufox, fastapi │ ├── run.sh ← script de arranque │ └── logs/ ← logs del worker └── .bin/svc ← wrapper para gestionar el service ``` **Otros archivos importantes:** - `/etc/systemd/system/api.service` — unit del API - `/etc/systemd/system/worker.service` — unit del worker - `/etc/ssh/sshd_config` — tiene un Match block hardcoded para `rastreador` con sudoers limitado a `systemctl`, `journalctl` - `/home/ubuntu/msc-navigator/site/shots/` — screenshots para el user (servido públicamente por otro side-project en msc-navigator) --- ## Credenciales y URLs críticas > **Cambiar estas si el operador rota claves o el server se mueve.** ``` URL base pública: https://miopenclaw-vnic.tail9799d2.ts.net/msc-api URL local API: http://127.0.0.1:9091 URL local worker: http://127.0.0.1:9090 Tailscale IP: 100.87.116.90 Hostname: miopenclaw-vnic.tail9799d2.ts.net Admin user: admin Admin password: camaronnochacarron ← (bootstrap_password en config.json, cambiar ASAP) Magic key: xppkMYzHIM0ZFO-O3TdgHQumGlhMhg3l ``` **Magic login (para operator only):** ``` curl -sk -H "X-Magic-Key: $MAGIC_KEY" \ https://miopenclaw-vnic.tail9799d2.ts.net/msc-api/api/magic-login?user=admin \ -c cookies.txt ``` ⚠️ **IMPORTANTE:** la key va en el header `X-Magic-Key`. NO en query param ni en URL (quedaría en logs de Tailscale y sería un hole de seguridad). ⚠️ **HISTORIAL:** antes de la v2 el endpoint aceptaba la key en query param y era trivial de explotar. Eso está arreglado desde junio 2026 — solo header. --- ## Cómo se opera ### Reiniciar servicios ```bash sudo systemctl restart api.service sudo systemctl restart worker.service sudo systemctl status api.service ``` ### Ver logs ```bash sudo journalctl -u api.service -n 50 # últimas 50 líneas sudo journalctl -u api.service -f # follow en vivo sudo journalctl -u worker.service -n 50 ``` ### Editar código - API: `/home/rastreador/api/app.py` → después `sudo systemctl restart api.service` - CSS: `/home/rastreador/api/static/style.css` (no necesita restart, se sirve static) - HTML: `/home/rastreador/api/static/*.html` (idem) - Worker: `/home/rastreador/worker/worker.py` → `sudo systemctl restart worker.service` > **Importante:** todos los archivos en `/home/rastreador/` son del user `rastreador`. Para editar, usar `sudo -u rastreador vim ...` o cambiar permisos primero. ### Backup manual ```bash sudo cp /home/rastreador/api/users.db ~/backups/users-$(date +%Y%m%d).db sudo cp /home/rastreador/api/state.json ~/backups/state-$(date +%Y%m%d).json sudo cp /home/rastreador/api/config.json ~/backups/config-$(date +%Y%m%d).json ``` > **No hay backup automático.** Es uno de los pendientes. ### Conectarse como `rastreador` (SSH user limitado) ```bash ssh -i ~/.ssh/msc-worker.key rastreador@ ``` Solo puede hacer `systemctl {restart,status}` y `journalctl`. Match block en `/etc/ssh/sshd_config`. --- ## Cómo debuggear ### Si un endpoint no anda 1. `sudo journalctl -u api.service -n 100` — ver el error 2. Test directo con curl: ```bash curl -sk https://miopenclaw-vnic.tail9799d2.ts.net/msc-api/api/me \ -H "X-Api-Key: admin:camaronnochacarron" ``` 3. Test local con curl: ```bash curl -s http://127.0.0.1:9091/api/me -H "X-Api-Key: admin:camaronnochacarron" ``` ### Si el worker no responde ```bash sudo journalctl -u worker.service -n 100 curl -s http://127.0.0.1:9090/health ``` ### Si la página web no carga bien 1. Hard refresh en browser (`Ctrl+Shift+R`) — Edge cachea fuerte 2. Ver consola del browser (F12) — buscar 404s, errores de JS 3. Si dice "Failed to load resource: 404" en favicon.ico, es cosmético, ignorar 4. Si dice 502 de Tailscale, esperar 30s (gateway OpenClaw a veces se reinicia) ### Si Playwright/Chrome no puede cargar el panel - Bug conocido de v2 que arreglé: el `checkAuth` del panel JS tiraba redirect a `/login` cuando algo fallaba. Fix: declaré `let me = null` una sola vez, paths absolutos con `BASE = '/msc-api'`, y check defensivo de DOM elements. - Si vuelve a pasar, ver `/home/rastreador/api/static/index.html` líneas 504–545 (API.req y checkAuth). ### Screenshots automatizados - Hay scripts Playwright en `/tmp/ui-*/` que tomé durante desarrollo - Chrome en `/home/rastreador/bin/chromium-bin/chromium-1217/chrome-linux/chrome` (v1217) - Para volver a tomar: ```python from playwright.sync_api import sync_playwright CHROME = '/home/rastreador/bin/chromium-bin/chromium-1217/chrome-linux/chrome' with sync_playwright() as p: b = p.chromium.launch(headless=True, executable_path=CHROME, args=['--no-sandbox','--disable-gpu','--disable-dev-shm-usage']) ... ``` --- ## Known issues / cosas en el tintero ### Bugs conocidos 1. ~~**llms.txt y llms-full.txt están desactualizados**~~ — **ARREGLADO en jun 2026.** Ahora describen auth v2 (user:pass), no exponen passwords, y el doc de magic-login dice header (no URL). 2. ~~**Magic-login era vulnerable a acceso no autenticado**~~ — **ARREGLADO en jun 2026.** Antes, el endpoint aceptaba la key en query param (o no la validaba) y cualquiera que supiera la URL se hacía admin. Ahora la key va **obligatoriamente en el header `X-Magic-Key`** y se valida con `secrets.compare_digest` (timing-safe). Si no hay header, da 403. Si la key está mal, da 403. Aceptar la key en query da 403 también (así no queda en logs). 3. **Sessions en DB no se limpian automáticamente** — solo se borran al check de expiración. Hay `cleanup_sessions()` en el scheduler pero no estoy 100% seguro de que esté corriendo bien. Considerar un cron diario que haga `DELETE FROM sessions WHERE expires_at < ?`. 4. **No hay favicon.ico** — el panel muestra 404 en console (cosmético, pero molesto). 5. **El track one-shot (`POST /api/track`) no tiene rate limiting** — Andres puede llamarlo muchas veces por segundo y que MSC lo banee. El código PHP que le pasamos tiene delay random de 20-60s, pero si cambia el código se rompe. 6. **El endpoint `/track/{container}` devuelve ~900KB de base64 screenshot** — cada llamada pesa ~1.3MB total. No hay caching. Si Andres lo llama cada 5 min por 5 containers = 130KB/minuto. No es problema, pero tenerlo en mente. ### Cosas que quedaron pendientes (no son bugs, son features/tareas) 1. **No probé la integración con el cron real de Andres.** El código PHP que está en `/docs/andres` debería andar, pero nunca lo ejecuté con un container real contra el endpoint. Cuando Andres lo pruebe, monitor los logs. 2. **No hay monitoreo/alerting.** Si el worker se cae a las 3am, nadie se entera. Considerar un healthcheck externo o un watchdog. 3. **No hay backup automático de `users.db` ni `state.json`.** Si se rompe el disco, se pierde todo. Hay un script manual en este doc, pero no está en cron. 4. **No hay per-user isolation de containers.** Cualquier user autenticado ve/modifica cualquier container. Para Andres, no es problema (es el único user no-admin), pero si se suman más users, habría que agregar `owner` a la tabla. 5. **El `bootstrap_password` está en plaintext en `config.json`.** Si alguien lee ese archivo, tiene la password del admin. Considerar leer de variable de entorno o Bitwarden. 6. **El magic key también está en plaintext.** Mismo problema. La idea era que sea solo accesible para el operador por Tailscale, pero está en el disco. 7. **La doc page `errors.html` menciona "Akamai ban" pero el worker probablemente no tiene esa lógica implementada.** Habría que verificar y agregar el rate-limit real al worker. 8. **El panel `index.html` tiene una sección "API para scripts" con curl examples que usan `https://HOST/...` como placeholder genérico.** Podría ser más útil con la URL real hardcoded. 9. **No hay tests automatizados.** Todo fue testeo manual con Playwright. Considerar pytest para el backend y Playwright tests para el frontend. 10. **Sessions cleanup.** El scheduler tiene `cleanup_sessions()` pero hay que verificar que corra. Hoy las sesiones expiradas solo se borran cuando alguien intenta usarlas. ### Decisiones de diseño que están bien pero hay que conocer - **`base_path = "/msc-api"` en config.json.** Todo el código lo usa para construir paths absolutos. Si Tailscale Funnel cambia, hay que cambiar este valor. - **Cookie `SameSite=Strict`.** Esto es estricto pero seguro. Si en el futuro se quiere usar el API desde otro origen (no el panel), va a fallar. - **Magic login solo funciona para `admin` user.** Hardcoded en el handler. Si en el futuro hay que darle magic-login a otros users, hay que cambiarlo. - **El scheduler corre cada 60s.** Si se agregan 100 containers, el scheduler va a hacer 100 requests a MSC en 60s = no va a funcionar. Hay que stagger. - **El worker devuelve 200 con `error: true, status: 4xx, detail: ...`** en caso de fallo. El scheduler interpreta `data["error"]` y guarda `last_status` y `last_error`. No es un raise, así que el scheduler sigue. Esto es OK pero confundir logs. - **Topnav tiene z-index 999, dropdowns z-index 9999.** El topnav es `position: sticky`. Si se mete un `position: fixed` o `transform` en algún hijo del main, se rompe el stacking context y el dropdown queda detrás. Lo aprendí en la última sesión. ### Cosas que el operador (Ignacio) quiere pero no me dijo explícitamente - Mantenerlo **simple**. Es un proyecto de él + un amigo, no escala. - **Tono argentino rioplatense** en la UI (vos, che, boludo, piola). Si el panel dice "usuario" en vez de "user", mejor. - **No meter features de más.** Si un user pide algo, validar que sea realmente necesario. --- ## Convenciones de código ### Estilo - **Backend (Python):** FastAPI + Pydantic. Async donde tiene sentido. Logging con `log.info()`. Sin type hints exhaustivos (es código de un solo dev, no un library). - **Frontend (HTML/CSS/JS):** server-rendered HTML con un toque de vanilla JS. NO React/Vue/etc. Si algo se puede hacer server-side, mejor. - **CSS:** dark theme por defecto. Variables en `:root` (`--bg`, `--fg`, `--accent`, etc). Class names en kebab-case. Mobile-last. - **JS:** vanilla, sin build step. `const API = {...}` para requests, `BASE = '/msc-api'` para paths. - **Tono de la UI:** español argentino rioplatense (vos, "querés", "tenés", "podés"). No inglés. ### Rutas - Páginas: `/`, `/login`, `/docs`, `/docs/{name}`, `/users` - API: `/api/...` - Andres: `/track/{container}` (público con auth) - Archivos estáticos: `/static/...` ### Auth en endpoints - Endpoints públicos: solo si no hay datos sensibles - Endpoints auth: usan `Depends(require_user)` o `Depends(require_admin)` - Hardcoded paths: usar `CONFIG.get("base_path", "/msc-api")` para los redirects ### Tests / validación - **No hay tests automatizados.** Validar manualmente con curl antes de cada deploy. - Después de cada cambio en `app.py`, hacer: 1. `sudo systemctl restart api.service` 2. `sudo journalctl -u api.service -n 20` — ver que arrancó sin error 3. `curl -sk https://.../api/me -H "X-Api-Key: admin:..."` — ver que responde 4. Hard refresh en browser y verificar visualmente --- ## Qué no se hizo (por si te lo preguntan) - ❌ No probé el código PHP real de Andres (no tengo acceso a su server) - ❌ No hay tests automatizados - ❌ No hay monitoring/alerting - ❌ No hay backup automático - ❌ No hay favicon - ❌ No hay per-user isolation - ❌ No hay rate-limiting real - ❌ No hay versionado de la API - ❌ El código no está en git (todo está en disco) - ❌ Sessions cleanup no verificado ## Cambios recientes en este handover - **jun 2026:** Bug crítico de magic-login arreglado (key en header, no en URL) - **jun 2026:** llms.txt y llms-full.txt regenerados con auth v2 (sin passwords hardcoded) - **jun 2026:** Endpoint `/handover.md` agregado al API para servir este doc por HTTP --- ## Prompt inicial sugerido para el siguiente agente > "Hola, recién tomo接手 este proyecto. Leí el `HANDOVER.md` que está en `/home/rastreador/HANDOVER.md`. Dame un status: (1) qué entendiste del proyecto, (2) qué archivos críticos miraste, (3) qué pendientes te parecen más urgentes, (4) cualquier cosa que no entiendas. Después de ese status, preguntame cuál es la primer instrucción." --- ## Si encontrás un error mid-trabajo 1. **Lo más probable:** bug en el JS del panel o path mal armado. Hard refresh (`Ctrl+Shift+R`) suele arreglar. 2. **Si es 502/504 de Tailscale:** el gateway OpenClaw se está reiniciando. Esperar 30s y reintentar. 3. **Si es 401 inesperado:** la cookie expiró. Re-login. 4. **Si el worker no responde:** `sudo systemctl restart worker.service`. Si sigue sin responder, ver logs. 5. **Si el API no responde:** `sudo systemctl restart api.service`. Si no arranca, `sudo journalctl -u api.service -n 50` te va a decir por qué. 6. **Si tocás algo y todo se rompe:** hay un backup de `app.py` en `app.py.bak-v2-users`. No es de v1, es de v2, pero por si acaso. --- ## Glosario - **MSC:** Mediterranean Shipping Company, la naviera que estamos scrapeando - **Container:** un container de carga (ej: `MEDU9730548`) - **BoL:** Bill of Lading (número de conocimiento de embarque) - **ETA:** Estimated Time of Arrival - **Akamai:** CDN que usa MSC; si nos banean, sale 403 - **Camoufox:** fork de Firefox con anti-detect fingerprinting - **Tailscale Funnel:** expone el server a internet público a través de la red Tailscale - **Magic login:** un endpoint que loguea al admin sin password (para operator only) - **Rate limit:** regla de "no más de 1 request / 5 min por container" para no ser baneados - **Sticky:** CSS `position: sticky` — el topnav se queda fijo arriba al scrollear --- **Última cosa:** si el operador (Ignacio) te pide algo que rompe convenciones o seguridad, **preguntale primero**. Es un proyecto chico y vale más la consistencia que la velocidad. Éxitos. 🤝