Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions app/Filament/Pages/M3uProxyStreamMonitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ public static function canAccess(): bool

public $systemStats = [];

public $vpnStatus = null;

public $refreshInterval = 5; // seconds

public $connectionError = null;
Expand Down Expand Up @@ -85,6 +87,10 @@ public function refreshData(): void
];

$this->systemStats = []; // populate if external API provides system metrics

// Fetch VPN watchdog status (non-blocking, null if not enabled)
$vpnResult = $this->apiService->fetchVpnStatus();
$this->vpnStatus = ($vpnResult['success'] ?? false) ? ($vpnResult['vpn'] ?? null) : null;
}

protected function getHeaderActions(): array
Expand Down Expand Up @@ -185,6 +191,33 @@ public function stopStream(string $streamId): void
$this->refreshData();
}

public function rotateVpn(): void
{
try {
$result = $this->apiService->triggerVpnRotation();
if ($result['success'] ?? false) {
Notification::make()
->title('VPN rotation triggered successfully.')
->success()
->send();
} else {
Notification::make()
->title('VPN rotation failed.')
->body($result['error'] ?? 'Unknown error')
->danger()
->send();
}
} catch (Exception $e) {
Notification::make()
->title('Error rotating VPN.')
->body($e->getMessage())
->danger()
->send();
}

$this->refreshData();
}

protected function getActiveStreams(): array
{
$apiStreams = $this->apiService->fetchActiveStreams();
Expand Down
10 changes: 10 additions & 0 deletions app/Http/Controllers/Api/M3uProxyApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,16 @@ public function handleWebhook(Request $request)
case 'stream_stopped':
$this->invalidateStreamCaches($data);
break;

case 'vpn_health_changed':
case 'vpn_rotation_started':
case 'vpn_rotation_completed':
case 'vpn_rotation_failed':
Log::info('VPN watchdog event received', [
'event_type' => $eventType,
'data' => $data,
]);
break;
}

return response()->json(['status' => 'ok']);
Expand Down
89 changes: 89 additions & 0 deletions app/Services/M3uProxyService.php
Original file line number Diff line number Diff line change
Expand Up @@ -2449,4 +2449,93 @@ public function getWebhookUrl(): ?string
// Return null if not configured, as webhooks are optional and may not be needed if the resolver URL is not set
return null;
}

/**
* Fetch VPN watchdog status from m3u-proxy.
*
* @return array{success: bool, error?: string, vpn?: array}
*/
public function fetchVpnStatus(): array
{
if (empty($this->apiBaseUrl)) {
return [
'success' => false,
'error' => 'M3U Proxy base URL is not configured',
];
}

try {
$endpoint = $this->apiBaseUrl.'/vpn/status';
$response = Http::timeout(5)->acceptJson()
->withHeaders($this->apiToken ? [
'X-API-Token' => $this->apiToken,
] : [])
->get($endpoint);

if ($response->status() === 404) {
return [
'success' => true,
'vpn' => null,
];
}

if ($response->successful()) {
return [
'success' => true,
'vpn' => $response->json() ?: [],
];
}

return [
'success' => false,
'error' => 'M3U Proxy returned status '.$response->status(),
];
} catch (Exception $e) {
return [
'success' => false,
'error' => 'Unable to connect to m3u-proxy: '.$e->getMessage(),
];
}
}

/**
* Trigger a manual VPN rotation via m3u-proxy.
*
* @return array{success: bool, error?: string}
*/
public function triggerVpnRotation(): array
{
if (empty($this->apiBaseUrl)) {
return [
'success' => false,
'error' => 'M3U Proxy base URL is not configured',
];
}

try {
$endpoint = $this->apiBaseUrl.'/vpn/rotate';
$response = Http::timeout(30)->acceptJson()
->withHeaders($this->apiToken ? [
'X-API-Token' => $this->apiToken,
] : [])
->post($endpoint);

if ($response->successful()) {
return [
'success' => true,
'data' => $response->json() ?: [],
];
}

return [
'success' => false,
'error' => $response->json()['detail'] ?? 'VPN rotation failed',
];
} catch (Exception $e) {
return [
'success' => false,
'error' => 'Unable to connect to m3u-proxy: '.$e->getMessage(),
];
}
}
}
59 changes: 59 additions & 0 deletions resources/views/filament/pages/m3u-proxy-stream-monitor.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,65 @@
</x-filament::card>
</div>

{{-- VPN Watchdog Status Card --}}
@if($vpnStatus)
<div class="mb-6">
<x-filament::card class="p-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
<div class="p-2 rounded-lg
@switch($vpnStatus['state'] ?? 'unknown')
@case('healthy') bg-green-100 dark:bg-green-900 @break
@case('degraded') bg-yellow-100 dark:bg-yellow-900 @break
@case('unhealthy') bg-orange-100 dark:bg-orange-900 @break
@case('down') bg-red-100 dark:bg-red-900 @break
@default bg-gray-100 dark:bg-gray-900
@endswitch
">
<x-heroicon-s-shield-check class="h-6 w-6" />
</div>
<div>
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">VPN Status</p>
<div class="flex items-center gap-2">
<span class="text-lg font-bold text-gray-900 dark:text-white capitalize">{{ $vpnStatus['state'] ?? 'Unknown' }}</span>
@if($vpnStatus['public_ip'] ?? null)
<x-filament::badge size="sm" color="info">{{ $vpnStatus['public_ip'] }}</x-filament::badge>
@endif
@if($vpnStatus['country'] ?? null)
<x-filament::badge size="sm">{{ $vpnStatus['country'] }}</x-filament::badge>
@endif
</div>
</div>
</div>
<div class="flex items-center gap-4">
@if($vpnStatus['latency_ms'] ?? null)
<div class="text-right">
<p class="text-xs text-gray-500 dark:text-gray-400">Latency</p>
<p class="text-sm font-semibold {{ ($vpnStatus['latency_ms'] ?? 0) > 200 ? 'text-yellow-600' : 'text-gray-900 dark:text-white' }}">
{{ round($vpnStatus['latency_ms']) }} ms
</p>
</div>
@endif
@if($vpnStatus['total_rotations'] ?? 0)
<div class="text-right">
<p class="text-xs text-gray-500 dark:text-gray-400">Rotations</p>
<p class="text-sm font-semibold text-gray-900 dark:text-white">{{ $vpnStatus['total_rotations'] }}</p>
</div>
@endif
<x-filament::button
size="sm"
color="warning"
wire:click="rotateVpn"
wire:confirm="Are you sure you want to rotate the VPN connection?"
>
Rotate VPN
</x-filament::button>
</div>
</div>
</x-filament::card>
</div>
@endif

<!-- Auto-refresh toggle -->
<div class="mb-4 flex items-center justify-between">
<div class="flex items-center space-x-4">
Expand Down
Loading