Skip to content
Closed
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
30 changes: 24 additions & 6 deletions app/components/admin/HomepageContent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,32 @@
</SettingsGroup>

<SettingsGroup
label="Status Badge"
label="Status Badge (Online)"
description="Text shown in the pill badge above the title."
>
<input
v-model="statusBadgeText"
v-model="statusBadgeTextOnline"
type="text"
maxlength="100"
class="w-full bg-bg-tertiary border border-border rounded px-3 py-2 text-sm text-text-primary focus:outline-none focus:border-white/20"
placeholder="Tracker Online & Operational"
/>
</SettingsGroup>
</div>

<div>
<label
class="text-[10px] font-bold uppercase tracking-widest text-text-muted block mb-2"
>
Status Badge (Offline)
</label>
<input
v-model="statusBadgeTextOffline"
type="text"
maxlength="100"
class="w-full bg-bg-tertiary border border-border rounded px-3 py-2 text-sm text-text-primary focus:outline-none focus:border-white/20"
placeholder="Tracker Offline"
/>
</div>
</div>

<!-- Feature Boxes -->
Expand Down Expand Up @@ -146,7 +161,8 @@ onMounted(async () => {
const settings = await $fetch<{
heroTitle: string;
heroSubtitle: string;
statusBadgeText: string;
statusBadgeTextOnline: string;
statusBadgeTextOffline: string;
feature1Title: string;
feature1Desc: string;
feature2Title: string;
Expand All @@ -157,7 +173,8 @@ onMounted(async () => {

heroTitle.value = settings.heroTitle;
heroSubtitle.value = settings.heroSubtitle;
statusBadgeText.value = settings.statusBadgeText;
statusBadgeTextOnline.value = settings.statusBadgeTextOnline;
statusBadgeTextOffline.value = settings.statusBadgeTextOffline;
features.value = [
{ title: settings.feature1Title, description: settings.feature1Desc },
{ title: settings.feature2Title, description: settings.feature2Desc },
Expand All @@ -176,7 +193,8 @@ async function saveContent() {
body: {
heroTitle: heroTitle.value,
heroSubtitle: heroSubtitle.value,
statusBadgeText: statusBadgeText.value,
statusBadgeTextOnline: statusBadgeTextOnline.value,
statusBadgeTextOffline: statusBadgeTextOffline.value,
feature1Title: features.value[0]?.title ?? '',
feature1Desc: features.value[0]?.description ?? '',
feature2Title: features.value[1]?.title ?? '',
Expand Down
10 changes: 9 additions & 1 deletion app/pages/admin.vue
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,12 @@
>
<span class="relative flex h-2 w-2">
<span
v-if="trackerOnline"
class="animate-ping absolute inline-flex h-full w-full rounded-full bg-success opacity-75"
></span>
<span
class="relative inline-flex rounded-full h-2 w-2 bg-success"
class="relative inline-flex rounded-full h-2 w-2"
:class="trackerOnline ? 'bg-success' : 'bg-error'"
></span>
</span>
LIVE TRACKER FEED
Expand Down Expand Up @@ -140,4 +142,10 @@ const currentItem = computed(

const currentTitle = computed(() => currentItem?.value?.label);
const currentDescription = computed(() => currentItem?.value?.description);

const { data: trackerStatus } = await useFetch('/api/tracker-status', {
server: false,
});

const trackerOnline = computed(() => trackerStatus.value?.online ?? false);
</script>
14 changes: 10 additions & 4 deletions app/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,17 @@
>
<span class="relative flex h-2 w-2">
<span
v-if="trackerOnline"
class="animate-ping absolute inline-flex h-full w-full rounded-full bg-success opacity-75"
></span>
<span
class="relative inline-flex rounded-full h-2 w-2 bg-success"
class="relative inline-flex rounded-full h-2 w-2"
:class="trackerOnline ? 'bg-success' : 'bg-error'"
></span>
</span>
<span
class="text-[10px] font-bold uppercase tracking-widest text-text-muted"
>{{
content?.statusBadgeText ?? 'Tracker Online & Operational'
}}</span
>{{ trackerOnline ? (content?.statusBadgeTextOnline ?? 'Tracker Online & Operational') : (content?.statusBadgeTextOffline ?? 'Tracker Offline') }}</span
>
</div>
<h1
Expand Down Expand Up @@ -202,6 +202,12 @@ const { data: torrentsData } = await useFetch<{ data: TorrentWithStats[] }>(
);

const recentTorrents = computed(() => torrentsData.value?.data ?? []);

const { data: trackerStatus } = await useFetch('/api/tracker-status', {
server: false,
});

const trackerOnline = computed(() => trackerStatus.value?.online ?? false);
</script>

<style>
Expand Down
9 changes: 6 additions & 3 deletions server/api/admin/settings.get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ import {
getAnnouncementType,
getHeroTitle,
getHeroSubtitle,
getStatusBadgeText,
getStatusBadgeTextOnline,
getStatusBadgeTextOffline,
getFeature1Title,
getFeature1Desc,
getFeature2Title,
Expand Down Expand Up @@ -60,7 +61,8 @@ export default defineEventHandler(async (event) => {
const announcementType = await getAnnouncementType();
const heroTitle = await getHeroTitle();
const heroSubtitle = await getHeroSubtitle();
const statusBadgeText = await getStatusBadgeText();
const statusBadgeTextOnline = await getStatusBadgeTextOnline();
const statusBadgeTextOffline = await getStatusBadgeTextOffline();
const feature1Title = await getFeature1Title();
const feature1Desc = await getFeature1Desc();
const feature2Title = await getFeature2Title();
Expand Down Expand Up @@ -92,7 +94,8 @@ export default defineEventHandler(async (event) => {
announcementType,
heroTitle,
heroSubtitle,
statusBadgeText,
statusBadgeTextOnline,
statusBadgeTextOffline,
feature1Title,
feature1Desc,
feature2Title,
Expand Down
8 changes: 6 additions & 2 deletions server/api/admin/settings.put.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,12 @@ export default defineEventHandler(async (event) => {
await setSetting(SETTINGS_KEYS.HERO_SUBTITLE, body.heroSubtitle);
}

if (typeof body.statusBadgeText === 'string') {
await setSetting(SETTINGS_KEYS.STATUS_BADGE_TEXT, body.statusBadgeText);
if (typeof body.statusBadgeTextOnline === 'string') {
await setSetting(SETTINGS_KEYS.STATUS_BADGE_TEXT_ONLINE, body.statusBadgeTextOnline);
}

if (typeof body.statusBadgeTextOffline === 'string') {
await setSetting(SETTINGS_KEYS.STATUS_BADGE_TEXT_OFFLINE, body.statusBadgeTextOffline);
}

if (typeof body.feature1Title === 'string') {
Expand Down
21 changes: 5 additions & 16 deletions server/api/admin/stats.get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { db, schema } from '../../db';
import { sql } from 'drizzle-orm';
import { redis } from '../../redis/client';
import { requireAdminSession } from '../../utils/adminAuth';
import { checkProtocolHealth } from '../../utils/protocolHealthCheck';

export default defineEventHandler(async (event) => {
// Require admin authentication
Expand Down Expand Up @@ -51,23 +52,11 @@ export default defineEventHandler(async (event) => {
console.error('[Stats] Failed to fetch peer count from Redis:', err);
}

// Try to get tracker, may fail if native modules aren't built
let tracker = null;
let protocols = { http: false, udp: false, ws: false };
// Check protocol health dynamically
const protocols = await checkProtocolHealth();

try {
const { getTracker } = await import('../../tracker');
tracker = getTracker();
if (tracker) {
protocols = {
http: !!tracker.http,
udp: !!tracker.udp,
ws: !!tracker.ws,
};
}
} catch {
// Tracker not available (native modules not built)
}
// Determine tracker status based on protocol health
const tracker = protocols.http || protocols.udp || protocols.ws;

return {
status: tracker ? 'running' : 'stopped',
Expand Down
9 changes: 6 additions & 3 deletions server/api/homepage-content.get.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import {
getHeroTitle,
getHeroSubtitle,
getStatusBadgeText,
getStatusBadgeTextOnline,
getStatusBadgeTextOffline,
getFeature1Title,
getFeature1Desc,
getFeature2Title,
Expand All @@ -17,7 +18,8 @@ import {
export default defineEventHandler(async () => {
const heroTitle = await getHeroTitle();
const heroSubtitle = await getHeroSubtitle();
const statusBadgeText = await getStatusBadgeText();
const statusBadgeTextOnline = await getStatusBadgeTextOnline();
const statusBadgeTextOffline = await getStatusBadgeTextOffline();
const feature1Title = await getFeature1Title();
const feature1Desc = await getFeature1Desc();
const feature2Title = await getFeature2Title();
Expand All @@ -28,7 +30,8 @@ export default defineEventHandler(async () => {
return {
heroTitle,
heroSubtitle,
statusBadgeText,
statusBadgeTextOnline,
statusBadgeTextOffline,
features: [
{ title: feature1Title, description: feature1Desc },
{ title: feature2Title, description: feature2Desc },
Expand Down
15 changes: 15 additions & 0 deletions server/api/tracker-status.get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { checkProtocolHealth } from '../utils/protocolHealthCheck';

/**
* GET /api/tracker-status
* Lightweight endpoint to check if tracker is online
* Used for real-time status indicators on homepage and admin panel
*/
export default defineEventHandler(async () => {
const protocols = await checkProtocolHealth();

return {
online: protocols.http || protocols.udp || protocols.ws,
protocols,
};
});
123 changes: 123 additions & 0 deletions server/utils/protocolHealthCheck.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/**
* Protocol Health Check Utility
* Dynamically checks if tracker protocols are actually responding
*/

export interface ProtocolStatus {
http: boolean;
udp: boolean;
ws: boolean;
}

/**
* Check if HTTP tracker is responding
*/
async function checkHttpHealth(): Promise<boolean> {
try {
// Use local tracker port for health check to avoid Cloudflare/proxy issues
const httpPort = process.env.TRACKER_HTTP_PORT || '8080';
const httpUrl = `http://localhost:${httpPort}`;

const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 2000); // 2 second timeout

try {
const response = await fetch(httpUrl, {
method: 'GET',
signal: controller.signal,
});
clearTimeout(timeoutId);

// Read response body
const text = await response.text();

// Valid tracker responses are bencoded and start with 'd'
if (text.startsWith('d')) {
return true;
}

// If we get here, it's not a valid tracker response
return false;
} catch (fetchError: any) {
clearTimeout(timeoutId);

// Timeout means server didn't respond
if (fetchError.name === 'AbortError') {
return false;
}

// Connection refused means server is down
if (fetchError.cause?.code === 'ECONNREFUSED' || fetchError.cause?.code === 'ECONNRESET') {
return false;
}

// Network errors mean server is unreachable
return false;
}
} catch (error) {
console.error('[Health Check] HTTP check failed:', error);
return false;
}
}

/**
* Check if UDP tracker is responding
* TODO: Implement UDP health check
* Port: 6969 (from TRACKER_UDP_URL)
*/
async function checkUdpHealth(): Promise<boolean> {
try {
const udpUrl = process.env.TRACKER_UDP_URL;
if (!udpUrl) {
return false;
}

// TODO: Implement UDP connection test
// This requires sending a UDP connect request to the tracker
// and waiting for a response
// For now, return false as UDP is disabled in the tracker config
return false;
} catch (error) {
console.error('[Health Check] UDP check failed:', error);
return false;
}
}

/**
* Check if WebSocket tracker is responding
* TODO: Implement WebSocket health check
*/
async function checkWsHealth(): Promise<boolean> {
try {
const wsUrl = process.env.TRACKER_WS_URL;
if (!wsUrl) {
return false;
}

// TODO: Implement WebSocket connection test
// This requires opening a WebSocket connection and checking if it connects
// For now, return false as WS is disabled in the tracker config
return false;
} catch (error) {
console.error('[Health Check] WebSocket check failed:', error);
return false;
}
}

/**
* Check all protocol health statuses
* Returns the current status of each protocol
*/
export async function checkProtocolHealth(): Promise<ProtocolStatus> {
const [http, udp, ws] = await Promise.all([
checkHttpHealth(),
checkUdpHealth(),
checkWsHealth(),
]);

return {
http,
udp,
ws,
};
}
Loading