osint-dashboard/alembic/versions/002_cameras.py
Sirius DevOps 69a30115cc
Some checks failed
build-and-deploy / build (push) Failing after 5s
Add open IP camera discovery scraper (OSINT map)
- camera_scraper: public-directory-only discovery (Insecam-style HTML +
  plain-text lists), hard private-range guard (fail closed), per-host
  rate limiting, Nominatim geocoding at <=1 req/s, TTL'd local snapshot
  cache, sha256 url_hash dedupe with Postgres upsert
- cameras table (migration 002) + /api/cameras?bbox= + snapshot endpoint
  with cache passthrough in main.py
- run_camera_service: long-running cycle worker following NATS->ingester
  pattern; publishes events.camera for shared ingester
- docker-compose camera-service profile, .env.example knobs

Verified E2E against TimescaleDB+PostGIS: private-range entries dropped,
dedupe across cycles holds, bbox query returns expected rows.
2026-08-24 14:54:51 -04:00

43 lines
1.6 KiB
Python

"""cameras table for open-camera discovery
Revision ID: 002_cameras
Revises: 001_initial
Create Date: 2026-08-24
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import UUID
revision = '002_cameras'
down_revision = '001_initial'
branch_labels = None
depends_on = None
def upgrade() -> None:
op.create_table(
'cameras',
sa.Column('id', UUID(as_uuid=True), primary_key=True),
sa.Column('url_hash', sa.String(64), nullable=False),
sa.Column('source_url', sa.Text(), nullable=False),
sa.Column('snapshot_url', sa.Text()),
sa.Column('discovery_source', sa.String(128), nullable=False),
sa.Column('location_lat', sa.Float()),
sa.Column('location_lon', sa.Float()),
sa.Column('location_name', sa.String(512)),
sa.Column('vendor', sa.String(128)),
sa.Column('device_type', sa.String(64)),
sa.Column('first_seen', sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.Column('last_seen', sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.Column('raw', sa.JSON()),
)
op.create_index('ix_cameras_location', 'cameras', ['location_lat', 'location_lon'])
op.create_index('ix_cameras_last_seen', 'cameras', ['last_seen'])
# Dedupe key. NOTE: cameras is intentionally NOT a Timescale hypertable —
# it holds current state per camera (one row per url_hash), not time-series.
op.create_index('ix_cameras_url_hash', 'cameras', ['url_hash'], unique=True)
def downgrade() -> None:
op.drop_table('cameras')