mirror of
https://github.com/pacnpal/simpleguardhome.git
synced 2025-12-19 20:11:14 -05:00
Add initial implementation of SimpleGuardHome web app with configuration and dependencies
This commit is contained in:
5
.env.example
Normal file
5
.env.example
Normal file
@@ -0,0 +1,5 @@
|
||||
# AdGuard Home Configuration
|
||||
ADGUARD_HOST=http://localhost
|
||||
ADGUARD_PORT=3000
|
||||
ADGUARD_USERNAME=admin
|
||||
ADGUARD_PASSWORD=password
|
||||
62
README.md
62
README.md
@@ -1,13 +1,65 @@
|
||||
# SimpleGuardHome
|
||||
**WARNING** Very early work in progress. Use at your own risk.
|
||||
|
||||
SimpleGuardHome is a Python project designed to provide a simple method to check if a domain is blocked by AdGuard Home and then add a custom rule for it.
|
||||
A simple web application for checking and managing domain filtering in AdGuard Home.
|
||||
|
||||
## Features
|
||||
|
||||
- Check if a domain is blocked by AdGuard Home.
|
||||
- Add a custom blocking rule for a domain in AdGuard Home.
|
||||
- Check if domains are blocked by your AdGuard Home instance
|
||||
- One-click domain unblocking
|
||||
- Modern, responsive web interface
|
||||
- Secure integration with AdGuard Home API
|
||||
|
||||
## Setup
|
||||
|
||||
1. Clone this repository:
|
||||
```bash
|
||||
git clone https://github.com/yourusername/simpleguardhome.git
|
||||
cd simpleguardhome
|
||||
```
|
||||
|
||||
2. Create a virtual environment and install dependencies:
|
||||
```bash
|
||||
python -m venv venv
|
||||
source venv/bin/activate # On Windows use: venv\Scripts\activate
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
3. Configure your environment:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Edit `.env` with your AdGuard Home instance details:
|
||||
```
|
||||
ADGUARD_HOST=http://localhost
|
||||
ADGUARD_PORT=3000
|
||||
ADGUARD_USERNAME=your_username
|
||||
ADGUARD_PASSWORD=your_password
|
||||
```
|
||||
|
||||
## Running the Application
|
||||
|
||||
Start the application:
|
||||
```bash
|
||||
python -m src.simpleguardhome.main
|
||||
```
|
||||
|
||||
Visit `http://localhost:8000` in your web browser.
|
||||
|
||||
## Usage
|
||||
|
||||
1. Enter a domain in the input field
|
||||
2. Click "Check Domain" or press Enter
|
||||
3. View the domain's blocking status
|
||||
4. If blocked, use the "Unblock Domain" button to whitelist it
|
||||
|
||||
## Development
|
||||
|
||||
The application is built with:
|
||||
- FastAPI for the backend
|
||||
- Tailwind CSS for styling
|
||||
- Modern JavaScript for frontend interactivity
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
||||
MIT License - See LICENSE file for details
|
||||
|
||||
10
requirements.txt
Normal file
10
requirements.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
fastapi==0.109.0
|
||||
uvicorn==0.27.0
|
||||
python-dotenv==1.0.0
|
||||
httpx==0.26.0
|
||||
pydantic==2.5.3
|
||||
pydantic-settings==2.1.0
|
||||
pytest==7.4.4
|
||||
pytest-asyncio==0.23.3
|
||||
python-multipart==0.0.6
|
||||
jinja2==3.1.3
|
||||
19
setup.py
Normal file
19
setup.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="simpleguardhome",
|
||||
version="0.1.0",
|
||||
packages=find_packages(),
|
||||
include_package_data=True,
|
||||
package_data={
|
||||
"simpleguardhome": ["templates/*"]
|
||||
},
|
||||
install_requires=[
|
||||
"fastapi",
|
||||
"uvicorn",
|
||||
"python-dotenv",
|
||||
"httpx",
|
||||
"pydantic",
|
||||
"jinja2",
|
||||
],
|
||||
)
|
||||
6
src/simpleguardhome/__init__.py
Normal file
6
src/simpleguardhome/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
"""SimpleGuardHome - Web app for managing AdGuard Home domain filtering."""
|
||||
|
||||
from .main import app, start
|
||||
|
||||
__version__ = "0.1.0"
|
||||
__all__ = ["app", "start"]
|
||||
140
src/simpleguardhome/adguard.py
Normal file
140
src/simpleguardhome/adguard.py
Normal file
@@ -0,0 +1,140 @@
|
||||
from typing import Dict, List, Optional
|
||||
import httpx
|
||||
import logging
|
||||
from .config import settings
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class AdGuardError(Exception):
|
||||
"""Base exception for AdGuard Home API errors."""
|
||||
pass
|
||||
|
||||
class AdGuardConnectionError(AdGuardError):
|
||||
"""Raised when connection to AdGuard Home fails."""
|
||||
pass
|
||||
|
||||
class AdGuardAPIError(AdGuardError):
|
||||
"""Raised when AdGuard Home API returns an error."""
|
||||
pass
|
||||
|
||||
class AdGuardClient:
|
||||
"""Client for interacting with AdGuard Home API."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the AdGuard Home API client."""
|
||||
self.base_url = settings.adguard_base_url
|
||||
self.client = httpx.AsyncClient(timeout=10.0) # 10 second timeout
|
||||
self._auth = None
|
||||
if settings.ADGUARD_USERNAME and settings.ADGUARD_PASSWORD:
|
||||
self._auth = (settings.ADGUARD_USERNAME, settings.ADGUARD_PASSWORD)
|
||||
logger.info(f"Initialized AdGuard Home client with base URL: {self.base_url}")
|
||||
|
||||
async def check_domain(self, domain: str) -> Dict:
|
||||
"""Check if a domain is blocked by AdGuard Home.
|
||||
|
||||
Args:
|
||||
domain: The domain to check
|
||||
|
||||
Returns:
|
||||
Dict containing the filtering status
|
||||
|
||||
Raises:
|
||||
AdGuardConnectionError: If connection to AdGuard Home fails
|
||||
AdGuardAPIError: If AdGuard Home API returns an error
|
||||
"""
|
||||
url = f"{self.base_url}/filtering/check_host"
|
||||
params = {"name": domain}
|
||||
|
||||
try:
|
||||
logger.info(f"Checking domain: {domain}")
|
||||
response = await self.client.get(url, params=params, auth=self._auth)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
logger.info(f"Domain check result for {domain}: {result}")
|
||||
return result
|
||||
|
||||
except httpx.ConnectError as e:
|
||||
logger.error(f"Connection error while checking domain {domain}: {str(e)}")
|
||||
raise AdGuardConnectionError(f"Failed to connect to AdGuard Home: {str(e)}")
|
||||
except httpx.HTTPError as e:
|
||||
logger.error(f"HTTP error while checking domain {domain}: {str(e)}")
|
||||
raise AdGuardAPIError(f"AdGuard Home API error: {str(e)}")
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error while checking domain {domain}: {str(e)}")
|
||||
raise AdGuardError(f"Unexpected error: {str(e)}")
|
||||
|
||||
async def get_filter_status(self) -> Dict:
|
||||
"""Get the current filtering status.
|
||||
|
||||
Returns:
|
||||
Dict containing the filtering status
|
||||
|
||||
Raises:
|
||||
AdGuardConnectionError: If connection to AdGuard Home fails
|
||||
AdGuardAPIError: If AdGuard Home API returns an error
|
||||
"""
|
||||
url = f"{self.base_url}/filtering/status"
|
||||
|
||||
try:
|
||||
logger.info("Getting filter status")
|
||||
response = await self.client.get(url, auth=self._auth)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
logger.info("Successfully retrieved filter status")
|
||||
return result
|
||||
|
||||
except httpx.ConnectError as e:
|
||||
logger.error(f"Connection error while getting filter status: {str(e)}")
|
||||
raise AdGuardConnectionError(f"Failed to connect to AdGuard Home: {str(e)}")
|
||||
except httpx.HTTPError as e:
|
||||
logger.error(f"HTTP error while getting filter status: {str(e)}")
|
||||
raise AdGuardAPIError(f"AdGuard Home API error: {str(e)}")
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error while getting filter status: {str(e)}")
|
||||
raise AdGuardError(f"Unexpected error: {str(e)}")
|
||||
|
||||
async def add_allowed_domain(self, domain: str) -> bool:
|
||||
"""Add a domain to the allowed list.
|
||||
|
||||
Args:
|
||||
domain: The domain to allow
|
||||
|
||||
Returns:
|
||||
bool: True if successful
|
||||
|
||||
Raises:
|
||||
AdGuardConnectionError: If connection to AdGuard Home fails
|
||||
AdGuardAPIError: If AdGuard Home API returns an error
|
||||
"""
|
||||
url = f"{self.base_url}/filtering/whitelist/add"
|
||||
data = {"name": domain}
|
||||
|
||||
try:
|
||||
logger.info(f"Adding domain to whitelist: {domain}")
|
||||
response = await self.client.post(url, json=data, auth=self._auth)
|
||||
response.raise_for_status()
|
||||
logger.info(f"Successfully added {domain} to whitelist")
|
||||
return True
|
||||
|
||||
except httpx.ConnectError as e:
|
||||
logger.error(f"Connection error while whitelisting domain {domain}: {str(e)}")
|
||||
raise AdGuardConnectionError(f"Failed to connect to AdGuard Home: {str(e)}")
|
||||
except httpx.HTTPError as e:
|
||||
logger.error(f"HTTP error while whitelisting domain {domain}: {str(e)}")
|
||||
raise AdGuardAPIError(f"AdGuard Home API error: {str(e)}")
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error while whitelisting domain {domain}: {str(e)}")
|
||||
raise AdGuardError(f"Unexpected error: {str(e)}")
|
||||
|
||||
async def close(self):
|
||||
"""Close the HTTP client."""
|
||||
await self.client.aclose()
|
||||
logger.info("Closed AdGuard Home client")
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
await self.close()
|
||||
22
src/simpleguardhome/config.py
Normal file
22
src/simpleguardhome/config.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from pydantic_settings import BaseSettings
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""Application settings using environment variables."""
|
||||
|
||||
ADGUARD_HOST: str = "http://localhost"
|
||||
ADGUARD_PORT: int = 3000
|
||||
ADGUARD_USERNAME: Optional[str] = None
|
||||
ADGUARD_PASSWORD: Optional[str] = None
|
||||
|
||||
@property
|
||||
def adguard_base_url(self) -> str:
|
||||
"""Get the base URL for AdGuard Home API."""
|
||||
return f"{self.ADGUARD_HOST}:{self.ADGUARD_PORT}"
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
|
||||
|
||||
settings = Settings()
|
||||
129
src/simpleguardhome/main.py
Normal file
129
src/simpleguardhome/main.py
Normal file
@@ -0,0 +1,129 @@
|
||||
from fastapi import FastAPI, Request, Form, HTTPException
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.responses import HTMLResponse, JSONResponse
|
||||
from pathlib import Path
|
||||
import httpx
|
||||
import logging
|
||||
from typing import Dict
|
||||
from . import adguard
|
||||
from .config import settings
|
||||
from .adguard import AdGuardError, AdGuardConnectionError, AdGuardAPIError
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
app = FastAPI(title="SimpleGuardHome")
|
||||
|
||||
# Setup templates directory
|
||||
templates_path = Path(__file__).parent / "templates"
|
||||
templates = Jinja2Templates(directory=str(templates_path))
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
async def home(request: Request):
|
||||
"""Render the home page."""
|
||||
return templates.TemplateResponse(
|
||||
"index.html",
|
||||
{"request": request}
|
||||
)
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check() -> Dict:
|
||||
"""Check the health of the application and AdGuard Home connection."""
|
||||
try:
|
||||
async with adguard.AdGuardClient() as client:
|
||||
status = await client.get_filter_status()
|
||||
return {
|
||||
"status": "healthy",
|
||||
"adguard_connection": "connected",
|
||||
"filtering_enabled": status.get("enabled", False)
|
||||
}
|
||||
except AdGuardConnectionError:
|
||||
return {
|
||||
"status": "degraded",
|
||||
"adguard_connection": "failed",
|
||||
"error": "Could not connect to AdGuard Home"
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Health check failed: {str(e)}")
|
||||
return {
|
||||
"status": "error",
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
@app.exception_handler(AdGuardError)
|
||||
async def adguard_exception_handler(request: Request, exc: AdGuardError) -> JSONResponse:
|
||||
"""Handle AdGuard-related exceptions."""
|
||||
if isinstance(exc, AdGuardConnectionError):
|
||||
status_code = 503 # Service Unavailable
|
||||
elif isinstance(exc, AdGuardAPIError):
|
||||
status_code = 502 # Bad Gateway
|
||||
else:
|
||||
status_code = 500 # Internal Server Error
|
||||
|
||||
return JSONResponse(
|
||||
status_code=status_code,
|
||||
content={
|
||||
"success": False,
|
||||
"error": exc.__class__.__name__,
|
||||
"detail": str(exc)
|
||||
}
|
||||
)
|
||||
|
||||
@app.post("/check-domain")
|
||||
async def check_domain(domain: str = Form(...)) -> Dict:
|
||||
"""Check if a domain is blocked by AdGuard Home."""
|
||||
if not domain:
|
||||
raise HTTPException(status_code=400, detail="Domain is required")
|
||||
|
||||
logger.info(f"Checking domain: {domain}")
|
||||
try:
|
||||
async with adguard.AdGuardClient() as client:
|
||||
result = await client.check_domain(domain)
|
||||
response = {
|
||||
"success": True,
|
||||
"domain": domain,
|
||||
"blocked": result.get("filtered", False),
|
||||
"rule": result.get("rule", ""),
|
||||
"filter_list": result.get("filter_list", "")
|
||||
}
|
||||
logger.info(f"Domain check result: {response}")
|
||||
return response
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking domain {domain}: {str(e)}")
|
||||
raise
|
||||
|
||||
@app.post("/unblock-domain")
|
||||
async def unblock_domain(domain: str = Form(...)) -> Dict:
|
||||
"""Add a domain to the allowed list."""
|
||||
if not domain:
|
||||
raise HTTPException(status_code=400, detail="Domain is required")
|
||||
|
||||
logger.info(f"Unblocking domain: {domain}")
|
||||
try:
|
||||
async with adguard.AdGuardClient() as client:
|
||||
await client.add_allowed_domain(domain)
|
||||
response = {
|
||||
"success": True,
|
||||
"domain": domain,
|
||||
"message": f"Successfully unblocked {domain}"
|
||||
}
|
||||
logger.info(f"Domain unblock result: {response}")
|
||||
return response
|
||||
except Exception as e:
|
||||
logger.error(f"Error unblocking domain {domain}: {str(e)}")
|
||||
raise
|
||||
|
||||
def start():
|
||||
"""Start the application using uvicorn."""
|
||||
import uvicorn
|
||||
uvicorn.run(
|
||||
"simpleguardhome.main:app",
|
||||
host="0.0.0.0",
|
||||
port=8000,
|
||||
reload=True
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
start()
|
||||
147
src/simpleguardhome/templates/index.html
Normal file
147
src/simpleguardhome/templates/index.html
Normal file
@@ -0,0 +1,147 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SimpleGuardHome</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
async function checkDomain(event) {
|
||||
event.preventDefault();
|
||||
const domain = document.getElementById('domain').value;
|
||||
const resultDiv = document.getElementById('result');
|
||||
const unblockDiv = document.getElementById('unblock-action');
|
||||
const submitBtn = document.getElementById('submit-btn');
|
||||
|
||||
try {
|
||||
// Show loading state
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = '<span class="inline-flex items-center">Checking... <svg class="animate-spin ml-2 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg></span>';
|
||||
|
||||
const response = await fetch('/check-domain', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: `domain=${encodeURIComponent(domain)}`
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
if (data.blocked) {
|
||||
resultDiv.innerHTML = `
|
||||
<div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 mb-4">
|
||||
<p class="font-bold">Domain is blocked</p>
|
||||
<p class="text-sm"><strong>${domain}</strong> is blocked by rule:</p>
|
||||
<p class="text-sm font-mono bg-red-50 p-2 mt-1 rounded">${data.rule || 'Unknown rule'}</p>
|
||||
${data.filter_list ? `<p class="text-sm mt-2">Filter List: ${data.filter_list}</p>` : ''}
|
||||
</div>`;
|
||||
unblockDiv.innerHTML = `
|
||||
<button onclick="unblockDomain('${domain}')"
|
||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded transition-colors duration-200">
|
||||
Unblock Domain
|
||||
</button>`;
|
||||
} else {
|
||||
resultDiv.innerHTML = `
|
||||
<div class="bg-green-100 border-l-4 border-green-500 text-green-700 p-4">
|
||||
<p class="font-bold">Domain is not blocked</p>
|
||||
<p class="text-sm"><strong>${domain}</strong> is allowed</p>
|
||||
</div>`;
|
||||
unblockDiv.innerHTML = '';
|
||||
}
|
||||
} else {
|
||||
// Show error message
|
||||
resultDiv.innerHTML = `
|
||||
<div class="bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700 p-4">
|
||||
<p class="font-bold">${data.error}</p>
|
||||
<p class="text-sm">${data.detail}</p>
|
||||
</div>`;
|
||||
unblockDiv.innerHTML = '';
|
||||
}
|
||||
} catch (error) {
|
||||
resultDiv.innerHTML = `
|
||||
<div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4">
|
||||
<p class="font-bold">Error checking domain</p>
|
||||
<p class="text-sm">${error.message}</p>
|
||||
</div>`;
|
||||
unblockDiv.innerHTML = '';
|
||||
} finally {
|
||||
// Reset button state
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = 'Check Domain';
|
||||
}
|
||||
}
|
||||
|
||||
async function unblockDomain(domain) {
|
||||
const resultDiv = document.getElementById('result');
|
||||
const unblockDiv = document.getElementById('unblock-action');
|
||||
const unblockBtn = unblockDiv.querySelector('button');
|
||||
|
||||
try {
|
||||
// Show loading state
|
||||
unblockBtn.disabled = true;
|
||||
unblockBtn.innerHTML = '<span class="inline-flex items-center">Unblocking... <svg class="animate-spin ml-2 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg></span>';
|
||||
|
||||
const response = await fetch('/unblock-domain', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: `domain=${encodeURIComponent(domain)}`
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
resultDiv.innerHTML = `
|
||||
<div class="bg-green-100 border-l-4 border-green-500 text-green-700 p-4">
|
||||
<p class="font-bold">Success!</p>
|
||||
<p class="text-sm">${data.message}</p>
|
||||
</div>`;
|
||||
unblockDiv.innerHTML = '';
|
||||
} else {
|
||||
resultDiv.innerHTML = `
|
||||
<div class="bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700 p-4">
|
||||
<p class="font-bold">${data.error}</p>
|
||||
<p class="text-sm">${data.detail}</p>
|
||||
</div>`;
|
||||
}
|
||||
} catch (error) {
|
||||
resultDiv.innerHTML = `
|
||||
<div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4">
|
||||
<p class="font-bold">Error unblocking domain</p>
|
||||
<p class="text-sm">${error.message}</p>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body class="bg-gray-100 min-h-screen">
|
||||
<div class="container mx-auto px-4 py-8 max-w-2xl">
|
||||
<h1 class="text-3xl font-bold text-center mb-8 text-gray-800">SimpleGuardHome</h1>
|
||||
|
||||
<div class="bg-white rounded-lg shadow-md p-6">
|
||||
<form onsubmit="checkDomain(event)" class="mb-6">
|
||||
<div class="mb-4">
|
||||
<label for="domain" class="block text-gray-700 text-sm font-bold mb-2">
|
||||
Enter Domain to Check
|
||||
</label>
|
||||
<input type="text" id="domain" name="domain" required
|
||||
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
||||
placeholder="example.com">
|
||||
</div>
|
||||
<button id="submit-btn" type="submit"
|
||||
class="bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded w-full transition-colors duration-200">
|
||||
Check Domain
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div id="result"></div>
|
||||
<div id="unblock-action" class="mt-4 text-center"></div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-center text-gray-600 text-sm">
|
||||
Make sure your AdGuard Home instance is running and properly configured in the .env file.
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user