OpenFlights dataset predates BER's 2020 opening. The patch already
existed in api_server.py for the search UI, but scan_processor.py
uses airports.py directly, so Germany scans silently skipped BER.
Added _MISSING_AIRPORTS to airports.py, patched both
get_airports_for_country() and _all_airports_by_iata() to inject
the extras, making BER available to scans and lookups.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a Rolling Window / Date Range toggle to the Parameters section.
Rolling Window mode (default) keeps the existing window_months stepper.
Date Range mode shows From/To date pickers and sends start_date/end_date
to the API instead of window_months. Validation covers empty dates,
past start dates, end ≤ start, and ranges exceeding 12 months.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add _recover_orphaned_new_tables() called at DB init startup: if scans_new
exists but scans doesn't (from a previously aborted migration), rename it back
- Drop VIEW recent_scans/active_scans before dropping the scans table in both
_migrate_add_pause_cancel_status and _migrate_add_reverse_scan_support, so
executescript can cleanly recreate them
- Add DROP TABLE IF EXISTS scans_new / scheduled_scans_new at the start of each
table-recreation migration to prevent 'table scans_new already exists' errors
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- DB schema: relaxed origin CHECK to >=2 chars, added scan_mode column to
scans and scheduled_scans, added origin_airport to routes and flights,
updated unique index to (scan_id, COALESCE(origin_airport,''), destination)
- Migrations: init_db.py recreates tables and adds columns via guarded ALTERs
- API: scan_mode field on ScanRequest/Scan; Route/Flight expose origin_airport;
GET /scans/{id}/flights accepts origin_airport filter; CreateScheduleRequest
and Schedule carry scan_mode; scheduler and run-now pass scan_mode through
- scan_processor: _write_route_incremental accepts origin_airport; process_scan
branches on scan_mode=reverse (country → airports × destinations × dates)
- Frontend: new CountrySelect component (populated from GET /api/v1/countries);
Scans page adds Direction toggle + CountrySelect for both modes; ScanDetails
shows Origin column for reverse scans and uses composite route keys; Re-run
preserves scan_mode
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Change API ORDER BY from price/date to date/price (chronological default)
- Add flightSortField/flightSortDir state to ScanDetails
- Make Date and Price sub-table headers clickable with sort icons
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
These files were referenced by ScanDetails.tsx but never committed,
breaking the Docker build (tsc could not resolve the imports).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Users running large scans can now pause (keep partial results, resume
later), cancel (stop permanently, partial results preserved), or resume
a paused scan which races through cache hits before continuing.
Backend:
- Extend scans.status CHECK to include 'paused' and 'cancelled'
- Add _migrate_add_pause_cancel_status() table-recreation migration
- scan_processor: _running_tasks/_cancel_reasons registries,
cancel_scan_task/pause_scan_task/stop_scan_task helpers,
CancelledError handler in process_scan(), start_resume_processor()
- api_server: POST /scans/{id}/pause|cancel|resume endpoints with
rate limits (30/min pause+cancel, 10/min resume); list_scans now
accepts paused/cancelled as status filter values
Frontend:
- Scan.status type extended with 'paused' | 'cancelled'
- scanApi.pause/cancel/resume added
- StatusChip: amber PauseCircle chip for paused, grey Ban for cancelled
- ScanDetails: context-aware action row with inline-confirm for
Pause and Cancel; Resume button for paused scans
Tests: 129 total (58 new) across test_scan_control.py,
test_scan_processor_control.py, and additions to existing suites
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a container is attached to multiple networks, Traefik picks an IP
from any of them. The frontend is on both domverse and flight-radar_default;
Traefik isn't on the latter, so ~50% of requests 504 after 30s.
Adding traefik.docker.network=domverse pins routing to the shared network.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Set ALLOWED_ORIGINS to production domain (fixes CORS for the web app)
- Add LOG_LEVEL=INFO to backend
- Add AutoKuma monitoring labels for Uptime Kuma auto-discovery
- Add Homepage dashboard labels (Productivity group)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
nginx:alpine is a minimal image that does not include wget.
Install curl explicitly and use curl -f for the healthcheck.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Without this file TypeScript errors on plain CSS side-effect imports
(e.g. import './index.css') because it lacks Vite's built-in type shims.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
tsconfig.json, tsconfig.app.json, and tsconfig.node.json were never
committed because *.json was gitignored without exceptions for them.
Added whitelist entries and restored Dockerfile to use npm run build
(tsc -b + vite) now that the config files are present.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
--spider sends a HEAD request which newer BusyBox builds in nginx:alpine
may not handle consistently. A plain GET to /dev/null is more reliable.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
On a fresh DB, migrations ran before the schema was applied and tried to
operate on tables that didn't exist yet (routes, scans), causing:
"no such table: routes" on _migrate_add_routes_unique_index
"no such table: scans" on _migrate_add_scheduled_scan_id_to_scans
Added table-existence checks so both migrations bail out when the table
isn't there yet. The schema's CREATE TABLE IF NOT EXISTS handles fresh
installs correctly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
tsc -b with project references fails in the Alpine Docker environment
(TypeScript 5.9 + no composite:true on referenced configs). Vite uses
esbuild for TS compilation anyway, so tsc -b only served as a type-check
which is redundant in a production build.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace .gitlab-ci.yml with .gitea/workflows/deploy.yml
- Fix Dockerfile.backend: add scan_processor.py and searcher_v3.py to
COPY command (they were missing, would cause runtime ImportError)
- Update docker-compose.yml comment to reference Gitea workflow
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
On every push to main: builds both Docker images on the server via
docker compose up --build -d, prunes dangling images, and prints the
running container list. No registry required — shell executor runner
on the deployment server is all that's needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New `scheduled_scans` table with daily/weekly/monthly frequencies
- asyncio background scheduler loop checks for due schedules every 60s
- 6 REST endpoints: CRUD + toggle enabled + run-now
- `scheduled_scan_id` FK added to scans table; migrated automatically
- Frontend: Schedules page (list + create form), Schedules nav link,
"Scheduled" badge on ScanDetails when scan was triggered by a schedule
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Specific-airports mode scans never resolved full airport names — they
stored the IATA code as destination_name. Fixed in two places:
- airports.py: add lookup_airport(iata) cached helper
- api_server.py: enrich destination_name/city on the fly in the routes
endpoint when the stored value equals the IATA code (fixes all past scans)
- scan_processor.py: resolve airport names at scan time in specific-airports
mode using lookup_airport (fixes future scans at the DB level)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the non-functional title attribute with a small Info icon
next to the IATA code badge. Hovering shows a dark tooltip with the
full airport name and city. Only rendered when useful name data exists.
Clicking the icon stops propagation so it doesn't expand the flights row.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Routes and individual flights are now written to the database as each
query result arrives, rather than after all queries finish. The frontend
already polls /scans/:id/routes while status=running, so routes appear
progressively with no frontend changes needed.
Changes:
- database/schema.sql: UNIQUE INDEX uq_routes_scan_dest(scan_id, destination)
- database/init_db.py: _migrate_add_routes_unique_index() migration
- scan_processor.py: _write_route_incremental() helper; progress_callback
now writes routes/flights immediately; Phase 2 bulk-write replaced with
a lightweight totals query
- searcher_v3.py: pass flights= kwarg to progress_callback on cache_hit
and api_success paths
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend:
- DELETE /api/v1/scans/{id} — 204 on success, 404 if missing,
409 if pending/running; CASCADE removes routes and flights
Frontend (api.ts):
- scanApi.delete(id)
Frontend (ScanDetails.tsx):
- Re-run button: derives window_months from stored dates, detects
country vs airports mode via comma in scan.country, creates new
scan and navigates to it; disabled while scan is active
- Delete button: inline two-step confirm (no modal), navigates to
dashboard on success; disabled while scan is active
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- docker-compose.dev.yml: backend on 8000, frontend (Vite) on 5173
- Backend mounts source files + uvicorn --reload for hot reload
- Frontend uses node:20-alpine, mounts ./frontend, runs npm run dev --host
- vite.config.ts: proxy target reads from API_TARGET env var
(defaults to localhost:8000 for plain npm run dev,
set to http://backend:8000 by docker-compose.dev.yml)
Usage: docker compose -f docker-compose.dev.yml up
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Single-container supervisord approach added unnecessary complexity.
Two containers is simpler and more standard:
- Dockerfile.backend: python:3.11-slim, uvicorn on port 8000
- Dockerfile.frontend: node build → nginx:alpine on port 80
- nginx.conf: proxy_pass restored to http://backend:8000
- docker-compose.yml: two services with depends_on
- Removed combined Dockerfile and supervisord.conf
Each container does one thing; logs are separate; either can be
restarted independently.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Rewrite airport search to use priority buckets instead of simple
append: exact IATA → IATA prefix → city prefix → city contains →
name prefix → name contains → country match. This ensures BER
appears before Berlin-Schönefeld when typing "BER".
- Add _MISSING_AIRPORTS patch list to get_airport_data() so airports
absent from the OpenFlights dataset (e.g. BER opened Nov 2020,
IST new Istanbul airport) are included at runtime.
- Deduplicate results via seen-set to avoid duplicate entries.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Added fastapi, uvicorn, pydantic, requests to requirements.txt
(were missing — only CLI deps were present)
- Changed fast-flights entry to git+GitHub URL (v3 not on PyPI)
- Added git to apt-get install in Dockerfile (needed for pip git+ URL)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace two-container setup (separate backend + nginx frontend) with a
single image that runs both via supervisord:
- New Dockerfile: Node stage builds React, Python+nginx stage is the runtime
- supervisord.conf: manages uvicorn (api_server.py) + nginx as sibling procs
- nginx.conf: proxy_pass updated to localhost:8000 (same container)
- docker-compose.yml: simplified to one service on port 80
Deploy:
docker-compose up -d # or
docker build -t flight-radar . && docker run -p 80:80 flight-radar
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each date row now shows e.g. "WED 2026-04-01" — the 3-letter weekday
prefix is rendered in muted monospace before the date string.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Auto-refresh toggle (RefreshCw icon, animates when active, polls every 5s)
- Horizontal filter bar: level select (140px) + search input + Clear button
- Clear button only rendered when level or search is active
- Level badges: INFO=blue, WARNING=amber, ERROR=red, CRITICAL=dark red, DEBUG=grey
- Row background tints: ERROR=#FFF5F5, WARNING=#FFFBF0, CRITICAL=#FFF0F0
- Message text in font-mono, metadata line with · separators
- Right-aligned timestamp: time on first line, date below
- Skeleton loading (8× SkeletonTableRow) while fetching
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>