Keitaro Remote Control frontend service
- Python 50.8%
- HTML 43.7%
- JavaScript 4.2%
- CSS 1%
- Dockerfile 0.3%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
- Extract ping error formatting into dedicated helper function for consistency - Handle upstream Keitaro error responses with fallback chain (response.error > response > payload.error) - Prefix HTTP status codes to error messages when available - Update flash message to show "OK" for successful pings instead of generic status - Enhance error display in health table with conditional rendering based on response type - Support both dict and string response payloads in template error rendering - Improve error message clarity and user experience in health check workflows |
||
| app | ||
| logs | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| config.py | ||
| docker-compose.yml | ||
| Dockerfile | ||
| pyproject.toml | ||
| pyrightconfig.json | ||
| README.md | ||
| run.py | ||
| uv.lock | ||
Keitaro Remote Control — Frontend
Flask web UI for managing Keitaro tracker instances via the KRC backend API. Internal admin tool with server-rendered pages, Bootstrap 5.3.8, no build step.
Tech Stack
| Layer | Technology |
|---|---|
| Framework | Flask 3.1.1 |
| Templates | Jinja2 + Bootstrap 5.3.8 (CDN) |
| HTTP Client | httpx 0.28.1 |
| Auth | PyJWT 2.12.1 (long-lived token, nginx basic auth) |
| Package Manager | uv |
| Production Server | Gunicorn 25.3.0 |
Architecture
Browser → nginx (basic auth) → Flask Frontend :8350 → FastAPI Backend :8300 → MariaDB / Keitaro
The frontend generates a long-lived JWT at startup (superadmin role) and uses it for all backend API calls. Access control is handled by nginx basic auth — no login form.
Layout
- Top navbar: global navigation (Instances, Reports, Health)
- Left sidebar: appears on instance-scoped pages with instance name and sub-navigation
- Full-width content: tables use the full viewport width for better readability
Modules
| Page | Path | Description |
|---|---|---|
| Instances | /instances |
CRUD for Keitaro tracker instances |
| Instance Detail | /instances/<id> |
Instance overview with edit/delete |
| API Keys | /instances/<id>/keys |
Manage API keys per instance |
| Campaigns | /instances/<id>/campaigns |
Campaign list with filter, pagination, detail modal (tabbed: Info, Parameters, Postbacks, Flows) |
| Offers | /instances/<id>/offers |
Offer list with filter, pagination, detail modal, download/upload |
| Landings | /instances/<id>/landings |
Landing pages list with pagination, group enrichment, download/upload |
| Traffic Sources | /instances/<id>/traffic-sources |
Traffic sources list |
| Affiliate Networks | /instances/<id>/affiliate-networks |
Affiliate networks list |
| Users | /instances/<id>/users |
Users list with detail modal |
| Domains | /instances/<id>/domains |
Domains list |
| Reports (per-instance) | /instances/<id>/reports/<type> |
Campaigns, offers, campaign-groups, affiliate-networks, landings, or traffic-sources report for the selected instance |
| Reports (global) | /reports/ and /reports/<type> |
Same report types aggregated across every active instance into one table, with a per-row "Instance" column |
| Conversions | /instances/<id>/conversions |
Conversions log with filters and pagination |
| Logs | /instances/<id>/logs |
Instance logs viewer with tabs per log type, search, pagination, and clean action |
| Health | /health |
Service status + ping all/single instances |
Features
Campaigns & Offers
- Server-side filter by ID (comma-separated, e.g.
?campaign_id=12,34) - Pagination (configurable page size via
PAGE_SIZEenv var) - Click any row to open a tabbed detail modal (Info, Parameters, Postbacks, Flows)
- Flows tab: lazily loaded via AJAX, shows flow cards with landings, offers (linked), filters, triggers
- Offers in flows: enriched with country, affiliate network, full local path (instance URL + offer path), and campaign domain; each offer has a "Copy to clipboard" button producing a tab-separated line for Excel paste, plus a last-7-days statistics table (clicks, unique clicks, cost, conversions, leads, rejected, sales, CR, approve, profit confirmed, ROI confirmed) from the reports API
- Offer and landing names resolved from backend (cached)
- Campaign IDs in reports link directly to the campaigns list (pre-filtered)
- Domain and Traffic Source names resolved from their respective APIs
- Campaign link generated from domain + alias (http/https based on domain SSL status)
- Download button for local offers/landings (streams zip via backend from Keitaro)
- Upload button for offers/landings (zip file, validated, base64-encoded to backend)
Landings
- Paginated list with group name enrichment
- Download landing archive (streamed zip from backend)
- Upload landing archive (zip validation + size limit)
Reports
Reports have one shared implementation used by both scopes:
- Per-instance (
/instances/<id>/reports/<type>) — scoped to a selected instance, in the instance sidebar. Behavior unchanged from before: same report types, filters, defaults, links, formatting, and client-side header sorting. - Global (
/reports/and/reports/<type>) — top-level navbar entry. Queries every active instance sequentially (one request per instance) and combines successful rows into a single full-width table with a leading Instance column. Each Instance cell links to that instance's own per-instance report of the same type, carrying over the current interval/date range. Rows are initially sorted by unique clicks descending; clicking any column header (including Instance) re-sorts client-side exactly like per-instance reports. If one or more instances fail, a warning names each failed instance and the report still displays rows from the successful ones; if all fail, an error is shown with no rows.
Shared across both scopes:
- Report types: campaigns, offers, campaign groups, affiliate networks, landings, traffic sources — same tabs, same order
- Date interval selector (today, yesterday, last 7 days, this month, last month, this year, last year, custom)
- Custom range: selecting "Custom" reveals from/to date pickers for an explicit date range
- Campaign report: optional filter by campaign/campaign-group IDs; Offers/Landings reports: optional filter by their IDs
- Clickable dimension IDs linking to entity list/report pages, resolved against the row's source instance
- Backend row limit per request is configurable independently per scope (see Configuration)
Conversions Log
- Date interval selector (same choices as reports, including custom range)
- Filter by campaign IDs (comma-separated)
- Filter by conversion status
- Server-side pagination with total count
- Columns: sub_id, campaign, campaign_id, campaign_group, offer, offer_id, landing_id, landing, ts, postback_datetime, status, country_code
Health
- Service health check endpoint
- Ping all instances at once
- Ping individual instance from any page (via referrer redirect)
Local Development
Requires uv and the backend running on port 8300.
# Start backend first (from ../backend/)
cd ../backend
uv sync
uv run fastapi dev app/main.py --host 0.0.0.0 --port 8300
# Then start frontend
cd ../frontend
cp .env.example .env
# Edit .env — set JWT_SECRET_KEY to match backend's JWT_SECRET_KEY
uv sync
uv run python run.py
Frontend runs on http://localhost:8350.
Configuration
All config via environment variables (.env file supported).
| Variable | Required | Default | Description |
|---|---|---|---|
JWT_SECRET_KEY |
Yes | changeme |
Shared JWT secret (must match backend) |
SECRET_KEY |
No | dev-secret |
Flask session secret |
BACKEND_URL |
No | http://localhost:8300 |
Backend API base URL |
APP_PORT |
No | 8350 |
Port to listen on |
PAGE_SIZE |
No | 1000 |
Items per page for paginated lists |
UPLOAD_MAX_SIZE_MB |
No | 50 |
Max upload file size in MB (offers/landings) |
UPLOAD_TIMEOUT_SECONDS |
No | 120 |
Timeout for upload requests to backend |
REPORT_ROW_LIMIT_PER_INSTANCE |
No | 25 |
Backend row limit per report request in per-instance scope |
REPORT_ROW_LIMIT_GLOBAL |
No | 25 |
Backend row limit per report request (applied per instance) in global scope |
LOGS_PAGE_SIZE |
No | 100 |
Number of log rows per page |
LOGS_CLEAN_ENABLED |
No | true |
Enable/disable log clean buttons (false/0/no to disable) |
Docker
# Standalone
docker build -t krc-frontend .
docker run -p 8350:8350 -e JWT_SECRET_KEY=your-secret -e BACKEND_URL=http://host:8300 krc-frontend
# With backend (docker-compose)
docker compose up -d
Project Structure
frontend/
├── app/
│ ├── __init__.py # App factory + JWT generation
│ ├── dashboard/ # / → redirect to instances
│ ├── health/ # Health dashboard
│ ├── instances/ # Instance CRUD
│ ├── keys/ # API key management
│ ├── campaigns/ # Campaign list (paginated, filterable, detail modal)
│ ├── offers/ # Offer list (paginated, filterable, detail modal, download/upload)
│ ├── landings/ # Landing pages (paginated, detail modal, download/upload)
│ ├── traffic_sources/ # Traffic sources list
│ ├── affiliate_networks/ # Affiliate networks list
│ ├── users/ # Users list (detail modal)
│ ├── domains/ # Domains list
│ ├── reports/ # Report views: one shared implementation for per-instance and global scope
│ │ ├── definitions.py # Immutable ReportDefinition per report type
│ │ └── orchestrator.py # render_report(scope, report_type, instance_id=None)
│ ├── conversions/ # Conversions log (paginated, filterable)
│ ├── logs/ # Instance logs viewer (tabbed, searchable, clean action)
│ ├── services/
│ │ └── api_client.py # Backend HTTP client
│ ├── intervals.py # Shared interval choices, get_interval() and build_range() helpers
│ ├── static/
│ │ ├── css/style.css # Custom styles (sidebar layout)
│ │ └── js/interval-filter.js # Toggles custom date fields for the interval selector
│ └── templates/
│ ├── base.html # Layout (navbar, sidebar, Bootstrap 5.3.8)
│ ├── _custom_date_inputs.html # Shared from/to date inputs for "Custom" interval
│ ├── campaigns/
│ ├── conversions/
│ ├── logs/
│ ├── domains/
│ ├── health/
│ ├── instances/
│ ├── keys/
│ ├── landings/
│ ├── offers/
│ ├── reports/
│ │ ├── report.html
│ │ ├── _tabs.html
│ │ ├── _filters.html
│ │ └── _table.html
│ ├── traffic_sources/
│ └── affiliate_networks/
├── config.py # Config class (env vars)
├── run.py # Dev entry point
├── pyproject.toml
├── uv.lock
├── Dockerfile
└── docker-compose.yml