The project follows Flask best practices with a modular architecture:
doh-switcher/
├── app/ # Main application package
│ ├── __init__.py # Flask app factory
│ ├── models.py # Data models and constants
│ ├── routes.py # Route handlers
│ ├── services/ # Business logic layer
│ │ ├── __init__.py
│ │ ├── database.py # Database operations
│ │ ├── doh_service.py # DoH service management
│ │ ├── monitoring.py # Background monitoring tasks
│ │ ├── network_service.py # Network utilities
│ │ └── provider_service.py # Provider CRUD operations
│ └── utils/ # Utility functions
│ ├── __init__.py
│ ├── decorators.py # Custom decorators (require_sudo)
│ ├── logging.py # Logging utilities
│ └── validators.py # URL validation logic
├── static/ # Static assets (CSS, JS)
│ └── css/
│ └── styles.css
├── templates/ # Jinja2 templates
│ ├── index.html
│ └── edit_provider.html
├── scripts/ # Legacy and utility scripts
│ ├── app.py.bak # Backup of monolithic app
│ └── change_dns.sh # Core DNS change script
├── Prerequisites/ # Installation prerequisites
├── config.py # Configuration management
├── run.py # Application entry point
├── setup.py # Package setup for installation
├── requirements.txt # Python dependencies
├── install.sh # Installation script
├── uninstall.sh # Uninstallation script
└── README.md # Main documentation
- Web UI using Jinja2 templates
- Tailwind CSS for styling
- WebSocket for real-time updates
- HTTP request handlers
- Form processing
- API endpoints
- Delegates to service layer
- doh_service.py: Manages cloudflared service operations
- provider_service.py: Provider CRUD and configuration
- network_service.py: Network diagnostics and ping operations
- database.py: SQLite database operations
- monitoring.py: Background tasks for real-time monitoring
- validators.py: URL validation and normalization
- decorators.py: Custom decorators (sudo checks)
- logging.py: Centralized logging
- SQLite Database: Stores ping history and DNS lookup records
- JSON Files: Provider configuration storage
- models.py: Constants and in-memory state
app/__init__.py uses the factory pattern:
def create_app():
app = Flask(__name__)
# Configure and initialize
return appBusiness logic is separated from routes:
# routes.py - presentation
@app.route("/select_provider", methods=["POST"])
def select_provider():
# Delegates to service
update_doh_service(url)
# doh_service.py - business logic
def update_doh_service(doh_url):
# Actual implementationReusable functionality via decorators:
@app.route("/api/status")
@require_sudo
def api_status():
# Protected route- User Request → Route Handler (
routes.py) - Route Handler → Service Layer (
services/) - Service Layer → External Systems (systemd, network, DB)
- Response ← Service Layer ← Route Handler
Uses environment variables with fallbacks:
# config.py
SECRET_KEY = os.getenv("SECRET_KEY", "default_key")
LOG_FILE = os.getenv("LOG_FILE", "/var/log/doh-switcher.log")ping_history
id: INTEGER PRIMARY KEYprovider: TEXT (DoH URL)time: TEXT (timestamp)ping: REAL (latency in ms)doh_ok: INTEGER (1=success, 0=failure)
dns_lookup_history
id: INTEGER PRIMARY KEYdomain: TEXTtime: TEXT (timestamp)result: TEXT (JSON array)
sudo python run.pysudo systemctl start doh-switcher
sudo systemctl enable doh-switcherRecommended test structure:
tests/
├── __init__.py
├── test_services/
│ ├── test_doh_service.py
│ ├── test_provider_service.py
│ └── test_network_service.py
├── test_utils/
│ └── test_validators.py
└── test_routes.py
The monolithic app.py (722 lines) has been refactored into:
app/__init__.py(48 lines) - App factoryapp/routes.py(389 lines) - Routes onlyapp/services/*.py(~200 lines) - Business logicapp/utils/*.py(~50 lines) - Utilities
This provides:
- Better separation of concerns
- Easier testing
- Improved maintainability
- Clearer code organization