Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@ VERIFIED_TOKEN_TTL_SECONDS=600
LOCKOUT_ATTEMPTS=5
LOCKOUT_SECONDS=600
MIN_SOLVE_MS=1200

INGEST_SCHEDULER_ENABLED=true
INGEST_SCHEDULER_INTERVAL_MS=86400000
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ coverage
apps/api/dist
packages/shared/dist
packages/widget/dist/*.map
demo/index.html
39 changes: 27 additions & 12 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
SHELL ?= /bin/zsh
DEMO_PORT ?= 3000

.PHONY: help install build lint test test-all migrate ingest ingest-docker sitekey dev setup setup-docker up down logs clean demo
.PHONY: help install build lint test test-integration test-all migrate ingest ingest-docker sitekey dev setup setup-docker up down logs clean demo demo-docker demo-prepare

help:
@echo "Targets disponibles:"
@echo " make install - Instala dependencias npm"
@echo " make build - Build de todos los paquetes"
@echo " make lint - Ejecuta checks de TypeScript"
@echo " make test - Ejecuta tests"
@echo " make test-integration - Ejecuta test live contra Missing Children"
@echo " make test-all - Ejecuta lint + tests (docker)"
@echo " make migrate - Ejecuta migraciones de DB"
@echo " make ingest - Carga casos desde Missing Children (local)"
Expand All @@ -20,8 +21,9 @@ help:
@echo " make up - Levanta docker compose (postgres + migrator + api)"
@echo " make down - Baja docker compose"
@echo " make logs - Logs de docker compose"
@echo " make clean - Borra node_modules y artefactos de build"
@echo " make demo - Prepara demo completa y sirve en http://localhost:$(DEMO_PORT)"
@echo " make clean - Borra node_modules, artefactos de build y datos locales de docker/db"
@echo " make demo - Prepara demo completa con datos reales y sirve en http://localhost:$(DEMO_PORT)"
@echo " make demo-docker - Prepara demo completa con datos reales y la sirve desde un contenedor docker temporal en http://localhost:$(DEMO_PORT)"

install:
@npm install
Expand All @@ -35,9 +37,13 @@ lint:
test:
@npm run --workspaces test

test-integration:
@npm run -w @missing-captcha/api test:integration

test-all:
@docker compose run --rm api npm run --workspaces lint
@docker compose run --rm api npm run --workspaces test
@docker compose run --rm api npm run -w @missing-captcha/api test:integration

migrate:
@npm run -w @missing-captcha/api migrate
Expand All @@ -55,7 +61,9 @@ setup: install up
@npm run -w @missing-captcha/api seed:sitekey "$(or $(DOMAINS),localhost)"
@npm run -w @missing-captcha/widget build

setup-docker: up ingest-docker
setup-docker: up
@docker compose exec -T api npm run -w @missing-captcha/api migrate
@${MAKE} ingest-docker
@docker compose exec -T api npm run -w @missing-captcha/api seed:sitekey "$(or $(DOMAINS),localhost)"
@docker compose exec -T api npm run -w @missing-captcha/widget build

Expand All @@ -77,14 +85,21 @@ logs:
clean:
@rm -rf node_modules apps/api/dist packages/shared/dist
@rm -f packages/widget/dist/widget.js packages/widget/dist/widget.css

demo: up
@PUBLIC_KEY=$$(docker compose exec -T api npm run -w @missing-captcha/api seed:sitekey "localhost" | grep -Eo 'rc_pk_[a-f0-9]+' | tail -n 1); \
if [ -z "$$PUBLIC_KEY" ]; then echo "No se pudo generar publicKey"; exit 1; fi; \
npm run -w @missing-captcha/widget build; \
mkdir -p demo; \
cp packages/widget/dist/widget.js demo/widget.js; \
cp packages/widget/dist/widget.css demo/widget.css; \
@docker compose down -v --remove-orphans

demo-prepare: up ingest-docker
@PUBLIC_KEY=$$(docker compose exec -T api npm run -w @missing-captcha/api seed:sitekey "localhost" | grep -Eo 'rc_pk_[a-f0-9]+' | tail -n 1) && \
if [ -z "$$PUBLIC_KEY" ]; then echo "No se pudo generar publicKey"; exit 1; fi && \
docker compose exec -T api npm run -w @missing-captcha/widget build && \
mkdir -p demo && \
docker compose exec -T api cat /app/packages/widget/dist/widget.js > demo/widget.js && \
docker compose exec -T api cat /app/packages/widget/dist/widget.css > demo/widget.css && \
sed "s/__PUBLIC_KEY__/$$PUBLIC_KEY/g" demo/index.template.html > demo/index.html

demo: demo-prepare
@echo "Demo lista en http://localhost:$(DEMO_PORT)"
@python3 -m http.server $(DEMO_PORT) -d demo

demo-docker: demo-prepare
@echo "Demo lista en http://localhost:$(DEMO_PORT)"
@docker run --rm -p $(DEMO_PORT):80 -v "$$PWD/demo:/usr/share/nginx/html:ro" nginx:alpine
45 changes: 37 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,26 @@ Monorepo con:
```sh
cp .env.example .env
make demo
# o
make demo-docker
```

Esto:

- levanta `postgres + migrator + api`
- refresca casos reales desde Missing Children en la DB local
- crea un `siteKey` para `localhost`
- compila `widget.js` y `widget.css`
- genera `demo/index.html`
- sirve la demo en `http://localhost:3000`

Nota: `make demo` ya no corre ingest automáticamente.
Diferencias:

- `make demo` sirve la carpeta `demo/` localmente con `python3 -m http.server`
- `make demo-docker` sirve la carpeta `demo/` desde un contenedor temporal `nginx:alpine`
- ambos generan el widget dentro del contenedor `api`, así no requieren `npm install` local para el build del demo

Nota: `make demo` y `make demo-docker` ejecutan ingest para refrescar los casos reales antes de servir la demo.

## Setup local

Expand All @@ -51,17 +60,36 @@ o
make ingest-docker
```

- Test live contra el sitio externo:

```sh
make test-integration
```

- El ingest consume datos públicos desde:
- `https://www.missingchildren.org.ar/listado.php?categoria=perdidos`
- `https://www.missingchildren.org.ar/listado.php?categoria=buscan`
- `https://www.missingchildren.org.ar/listado.php?categoria=mayores`
- El script recorre fichas individuales, toma la primera foto y extrae el bloque de contacto previo a `SI TIENE ALGUNA INFORMACIÓN CONTÁCTENOS:`.
- Durante la ejecución muestra progreso por categoría y aplica timeout por request para evitar bloqueos largos por red.
- `https://www.missingchildren.org.ar/pages/galeria_ajax.php?limite=<N>&offset=<N>&situacionBusqueda=perdidos`
- `https://www.missingchildren.org.ar/pages/detalles.php?id=<id>`
- El ingest usa un parser HTML real (`cheerio`) para leer la galería y las fichas de detalle.
- El sync es incremental por `source_external_id`: inserta nuevos casos, actualiza existentes, elimina casos marcados como encontrados y también elimina casos ausentes cuando su `updated_at` supera el umbral configurado.
- `contact_info` se llena con líneas fallback normalizadas (por ejemplo teléfono + email) y el CTA de WhatsApp se guarda en `report_url`.
- Durante la ejecución muestra progreso por página y aplica timeout por request para evitar bloqueos largos por red.
- Timeout configurable con `INGEST_REQUEST_TIMEOUT_MS` (default: `12000` ms).
- El API ya incluye un scheduler interno diario. Al arrancar, corre ingest solo si la DB todavía no tiene casos. Variables:
- `INGEST_SCHEDULER_ENABLED` (default: `true`)
- `INGEST_SCHEDULER_INTERVAL_MS` (default: `86400000`)
- `INGEST_DELETE_MISSING_AFTER_DAYS` (default: `30`)

## Tests

```sh
make test
make test-integration
make test-all
```

Roadmap:
- Agregar un cron job (por ejemplo diario) para refrescar la base automáticamente sin depender de ejecución manual.
- `make test` corre la suite normal.
- `make test-integration` corre un test real contra Missing Children (galería + detalle).
- `make test-all` corre lint, tests normales y el test live dentro de docker.

## Embed snippet

Expand Down Expand Up @@ -104,6 +132,7 @@ Roadmap:
## Notas

- Provider actual es demo (`apps/api/src/jobs/ingest_missing_children.ts`) con datos públicos/no sensibles.
- La selección de casos en runtime ya no rota por categorías; el origen activo se simplificó a `perdidos`.
- Para producción: reemplazar ingest demo por feed/API acordado y agregar A11y avanzada + métricas.

## Troubleshooting rápido
Expand Down
8 changes: 8 additions & 0 deletions apps/api/migrations/003_cases_external_fields.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
ALTER TABLE cases
ADD COLUMN IF NOT EXISTS source_external_id INTEGER,
ADD COLUMN IF NOT EXISTS gender TEXT,
ADD COLUMN IF NOT EXISTS birth_date DATE;

CREATE UNIQUE INDEX IF NOT EXISTS idx_cases_org_source_external_id
ON cases(organization_id, source_external_id)
WHERE source_external_id IS NOT NULL;
2 changes: 2 additions & 0 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@
"seed:sitekey": "tsx src/scripts/create_site_key.ts",
"ingest:missing": "tsx src/jobs/ingest_missing_children.ts",
"test": "node --test --import tsx test/*.test.ts",
"test:integration": "node --test --import tsx test/*.integration.ts",
"lint": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@fastify/cors": "^10.0.1",
"@fastify/rate-limit": "^10.3.0",
"@fastify/sensible": "^5.6.0",
"@missing-captcha/shared": "0.1.0",
"cheerio": "^1.2.0",
"dotenv": "^16.4.7",
"drizzle-orm": "^0.45.1",
"fastify": "^5.2.1",
Expand Down
3 changes: 3 additions & 0 deletions apps/api/src/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,11 @@ export const siteKeys = pgTable("site_keys", {
export const cases = pgTable("cases", {
id: uuid("id").primaryKey().defaultRandom(),
organizationId: uuid("organization_id"),
sourceExternalId: integer("source_external_id"),
name: text("name").notNull(),
age: integer("age"),
gender: text("gender"),
birthDate: date("birth_date", { mode: "date" }),
photoUrl: text("photo_url"),
lastSeenDate: date("last_seen_date", { mode: "date" }),
lastSeenLocationCity: text("last_seen_location_city"),
Expand Down
2 changes: 2 additions & 0 deletions apps/api/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,6 @@ export const env = {
LOCKOUT_ATTEMPTS: Number(process.env.LOCKOUT_ATTEMPTS ?? 5),
LOCKOUT_SECONDS: Number(process.env.LOCKOUT_SECONDS ?? 600),
MIN_SOLVE_MS: Number(process.env.MIN_SOLVE_MS ?? 1200),
INGEST_SCHEDULER_ENABLED: (process.env.INGEST_SCHEDULER_ENABLED ?? "true").toLowerCase() !== "false",
INGEST_SCHEDULER_INTERVAL_MS: Number(process.env.INGEST_SCHEDULER_INTERVAL_MS ?? 86400000),
};
Loading
Loading