First
Some checks failed
Build All Docker Images / changes (push) Has been cancelled
Build and Push App Docker Image / build (push) Has been cancelled
Build and Push Node Docker Image / build (push) Has been cancelled
Test and Lint / test-app (push) Has been cancelled
Test and Lint / test-node (push) Has been cancelled
Test and Lint / lint-dockerfiles (push) Has been cancelled
Test and Lint / security-scan (push) Has been cancelled
Build All Docker Images / build-app (push) Has been cancelled
Build All Docker Images / build-node (push) Has been cancelled
Build All Docker Images / summary (push) Has been cancelled

This commit is contained in:
hunternick87 2025-07-03 15:50:13 -04:00
commit 4169337dd0
68 changed files with 8726 additions and 0 deletions

View file

@ -0,0 +1,213 @@
import React from 'react';
import { useQuery } from '@tanstack/react-query';
import { tunnelApi, frpcApi, nodeApi } from '../api/client';
import { CheckCircle, XCircle, AlertCircle, RefreshCw, Wifi, WifiOff } from 'lucide-react';
const Dashboard: React.FC = () => {
const { data: tunnels, isLoading: tunnelsLoading } = useQuery({
queryKey: ['tunnels'],
queryFn: tunnelApi.getAllTunnels,
refetchInterval: 5000,
});
const { data: tunnelStatuses, isLoading: statusLoading } = useQuery({
queryKey: ['tunnel-statuses'],
queryFn: tunnelApi.getAllTunnelStatuses,
refetchInterval: 10000,
});
const { data: frpcStatus, isLoading: frpcLoading } = useQuery({
queryKey: ['frpc-status'],
queryFn: frpcApi.getStatus,
refetchInterval: 5000,
});
const { data: nodeConnection } = useQuery({
queryKey: ['node-connection'],
queryFn: nodeApi.getConnection,
refetchInterval: 30000,
retry: false,
});
const { data: nodeStatus } = useQuery({
queryKey: ['node-status'],
queryFn: nodeApi.getStatus,
refetchInterval: 30000,
retry: false,
enabled: !!nodeConnection?.isReachable,
});
const activeTunnels = tunnels?.filter(t => t.enabled) || [];
const activeTunnelStatuses = tunnelStatuses?.filter(s => s.active) || [];
if (tunnelsLoading || statusLoading || frpcLoading) {
return (
<div className="dashboard">
<div className="loading">
<RefreshCw className="animate-spin" size={24} />
<span>Loading dashboard...</span>
</div>
</div>
);
}
return (
<div className="dashboard">
<div className="dashboard-header">
<h1>FRP Manager Dashboard</h1>
<p>Manage your tunnel configurations and monitor their status</p>
</div>
<div className="stats-grid">
<div className="stat-card">
<div className="stat-icon">
<CheckCircle className="text-green-500" size={24} />
</div>
<div className="stat-content">
<h3>Active Tunnels</h3>
<p className="stat-number">{activeTunnelStatuses.length}</p>
<p className="stat-description">Currently running</p>
</div>
</div>
<div className="stat-card">
<div className="stat-icon">
<AlertCircle className="text-yellow-500" size={24} />
</div>
<div className="stat-content">
<h3>Total Tunnels</h3>
<p className="stat-number">{tunnels?.length || 0}</p>
<p className="stat-description">Configured tunnels</p>
</div>
</div>
<div className="stat-card">
<div className="stat-icon">
{frpcStatus?.running ? (
<CheckCircle className="text-green-500" size={24} />
) : (
<XCircle className="text-red-500" size={24} />
)}
</div>
<div className="stat-content">
<h3>FRPC Service</h3>
<p className="stat-number">{frpcStatus?.running ? 'Running' : 'Stopped'}</p>
<p className="stat-description">Service status</p>
</div>
</div>
<div className="stat-card">
<div className="stat-icon">
<CheckCircle className="text-blue-500" size={24} />
</div>
<div className="stat-content">
<h3>Enabled Tunnels</h3>
<p className="stat-number">{activeTunnels.length}</p>
<p className="stat-description">Ready to connect</p>
</div>
</div>
<div className="stat-card">
<div className="stat-icon">
{nodeConnection?.isReachable ? (
<Wifi className="text-green-500" size={24} />
) : (
<WifiOff className="text-red-500" size={24} />
)}
</div>
<div className="stat-content">
<h3>Node Status</h3>
<p className="stat-number">{nodeConnection?.isReachable ? 'Online' : 'Offline'}</p>
<p className="stat-description">Home server agent</p>
</div>
</div>
</div>
<div className="dashboard-sections">
<div className="section">
<h2>Recent Tunnels</h2>
<div className="tunnel-list">
{activeTunnels.slice(0, 5).map(tunnel => (
<div key={tunnel.id} className="tunnel-item">
<div className="tunnel-info">
<h4>{tunnel.name}</h4>
<p>{tunnel.protocol} {tunnel.localIp}:{tunnel.localPort} :{tunnel.remotePort}</p>
</div>
<div className="tunnel-status">
{tunnelStatuses?.find(s => s.id === tunnel.id)?.active ? (
<span className="status-badge status-active">Active</span>
) : (
<span className="status-badge status-inactive">Inactive</span>
)}
</div>
</div>
))}
{activeTunnels.length === 0 && (
<p className="empty-state">No active tunnels configured</p>
)}
</div>
</div>
<div className="section">
<h2>System Status</h2>
<div className="status-list">
<div className="status-item">
<div className="status-indicator">
{frpcStatus?.running ? (
<CheckCircle className="text-green-500" size={20} />
) : (
<XCircle className="text-red-500" size={20} />
)}
</div>
<div className="status-content">
<h4>FRPC Service</h4>
<p>{frpcStatus?.running ? 'Service is running normally' : 'Service is stopped'}</p>
</div>
</div>
<div className="status-item">
<div className="status-indicator">
<CheckCircle className="text-green-500" size={20} />
</div>
<div className="status-content">
<h4>API Server</h4>
<p>API is responding normally</p>
</div>
</div>
<div className="status-item">
<div className="status-indicator">
<CheckCircle className="text-green-500" size={20} />
</div>
<div className="status-content">
<h4>Database</h4>
<p>Database connection is healthy</p>
</div>
</div>
<div className="status-item">
<div className="status-indicator">
{nodeConnection?.isReachable ? (
<Wifi className="text-green-500" size={20} />
) : (
<WifiOff className="text-red-500" size={20} />
)}
</div>
<div className="status-content">
<h4>Home Server Node</h4>
<p>
{nodeConnection?.isReachable
? `Connected • Last seen: ${nodeConnection.lastConnectionTime ? new Date(nodeConnection.lastConnectionTime).toLocaleTimeString() : 'Now'}`
: 'Disconnected or unreachable'
}
</p>
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default Dashboard;

View file

@ -0,0 +1,253 @@
import React, { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { frpcApi } from '../api/client';
import { Server, Play, Square, RotateCcw, RefreshCw, FileText, Activity } from 'lucide-react';
import { toast } from 'react-hot-toast';
const ServerStatus: React.FC = () => {
const [logsLines, setLogsLines] = useState(50);
const queryClient = useQueryClient();
const { data: frpcStatus, isLoading: statusLoading } = useQuery({
queryKey: ['frpc-status'],
queryFn: frpcApi.getStatus,
refetchInterval: 3000,
});
const { data: logs, isLoading: logsLoading } = useQuery({
queryKey: ['frpc-logs', logsLines],
queryFn: () => frpcApi.getLogs(logsLines),
refetchInterval: 5000,
});
const startMutation = useMutation({
mutationFn: frpcApi.start,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['frpc-status'] });
toast.success('FRPC service started');
},
onError: (error) => {
console.error('Start error:', error);
toast.error('Failed to start FRPC service');
},
});
const stopMutation = useMutation({
mutationFn: frpcApi.stop,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['frpc-status'] });
toast.success('FRPC service stopped');
},
onError: (error) => {
console.error('Stop error:', error);
toast.error('Failed to stop FRPC service');
},
});
const restartMutation = useMutation({
mutationFn: frpcApi.restart,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['frpc-status'] });
toast.success('FRPC service restarted');
},
onError: (error) => {
console.error('Restart error:', error);
toast.error('Failed to restart FRPC service');
},
});
const regenerateMutation = useMutation({
mutationFn: frpcApi.regenerate,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['frpc-status'] });
toast.success('FRPC configuration regenerated');
},
onError: (error) => {
console.error('Regenerate error:', error);
toast.error('Failed to regenerate FRPC configuration');
},
});
const handleStart = () => startMutation.mutate();
const handleStop = () => stopMutation.mutate();
const handleRestart = () => restartMutation.mutate();
const handleRegenerate = () => regenerateMutation.mutate();
const isLoading = statusLoading || logsLoading;
const isAnyMutationPending =
startMutation.isPending ||
stopMutation.isPending ||
restartMutation.isPending ||
regenerateMutation.isPending;
return (
<div className="server-status">
<div className="server-header">
<h1>Server Status</h1>
<div className="server-info">
<div className="status-indicator">
<Activity
size={24}
className={frpcStatus?.running ? 'text-green-500' : 'text-red-500'}
/>
<span className={`status-text ${frpcStatus?.running ? 'text-green-500' : 'text-red-500'}`}>
{frpcStatus?.running ? 'Running' : 'Stopped'}
</span>
</div>
</div>
</div>
<div className="server-controls">
<div className="control-section">
<h2>Service Controls</h2>
<div className="control-buttons">
<button
className="btn btn-success"
onClick={handleStart}
disabled={isAnyMutationPending || frpcStatus?.running}
>
<Play size={18} />
Start
</button>
<button
className="btn btn-danger"
onClick={handleStop}
disabled={isAnyMutationPending || !frpcStatus?.running}
>
<Square size={18} />
Stop
</button>
<button
className="btn btn-warning"
onClick={handleRestart}
disabled={isAnyMutationPending}
>
<RotateCcw size={18} />
Restart
</button>
<button
className="btn btn-secondary"
onClick={handleRegenerate}
disabled={isAnyMutationPending}
>
<RefreshCw size={18} />
Regenerate Config
</button>
</div>
</div>
<div className="status-section">
<h2>Service Information</h2>
<div className="info-grid">
<div className="info-item">
<span className="info-label">Status:</span>
<span className={`info-value ${frpcStatus?.running ? 'text-green-500' : 'text-red-500'}`}>
{frpcStatus?.running ? 'Running' : 'Stopped'}
</span>
</div>
<div className="info-item">
<span className="info-label">Container:</span>
<span className="info-value">frpc</span>
</div>
<div className="info-item">
<span className="info-label">Last Updated:</span>
<span className="info-value">
{new Date().toLocaleString()}
</span>
</div>
</div>
</div>
</div>
<div className="logs-section">
<div className="logs-header">
<h2>
<FileText size={20} />
Service Logs
</h2>
<div className="logs-controls">
<select
value={logsLines}
onChange={(e) => setLogsLines(parseInt(e.target.value))}
className="logs-select"
>
<option value={25}>Last 25 lines</option>
<option value={50}>Last 50 lines</option>
<option value={100}>Last 100 lines</option>
<option value={200}>Last 200 lines</option>
</select>
<button
className="btn btn-sm btn-secondary"
onClick={() => queryClient.invalidateQueries({ queryKey: ['frpc-logs'] })}
disabled={logsLoading}
>
<RefreshCw size={16} />
Refresh
</button>
</div>
</div>
<div className="logs-container">
{logsLoading ? (
<div className="logs-loading">
<RefreshCw className="animate-spin" size={20} />
<span>Loading logs...</span>
</div>
) : (
<pre className="logs-content">
{logs?.logs || 'No logs available'}
</pre>
)}
</div>
</div>
<div className="system-info">
<h2>System Information</h2>
<div className="info-cards">
<div className="info-card">
<div className="info-card-header">
<Server size={24} />
<h3>FRPC Service</h3>
</div>
<div className="info-card-content">
<p>Fast Reverse Proxy Client for tunneling services</p>
<p className="info-detail">
Status: <span className={frpcStatus?.running ? 'text-green-500' : 'text-red-500'}>
{frpcStatus?.running ? 'Active' : 'Inactive'}
</span>
</p>
</div>
</div>
<div className="info-card">
<div className="info-card-header">
<Activity size={24} />
<h3>API Server</h3>
</div>
<div className="info-card-content">
<p>RESTful API for managing tunnel configurations</p>
<p className="info-detail">
Status: <span className="text-green-500">Running</span>
</p>
</div>
</div>
<div className="info-card">
<div className="info-card-header">
<FileText size={24} />
<h3>Configuration</h3>
</div>
<div className="info-card-content">
<p>Tunnel configurations stored in SQLite database</p>
<p className="info-detail">
Auto-generated FRPC config from active tunnels
</p>
</div>
</div>
</div>
</div>
</div>
);
};
export default ServerStatus;

View file

@ -0,0 +1,244 @@
import React, { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { tunnelApi, nodeApi, TunnelConfig } from '../api/client';
import { Plus, Edit2, Trash2, Power, PowerOff, Settings, Send, RefreshCw } from 'lucide-react';
import { toast } from 'react-hot-toast';
import TunnelForm from '../components/TunnelForm';
const TunnelManager: React.FC = () => {
const [showForm, setShowForm] = useState(false);
const [editingTunnel, setEditingTunnel] = useState<TunnelConfig | null>(null);
const queryClient = useQueryClient();
const { data: tunnels, isLoading } = useQuery({
queryKey: ['tunnels'],
queryFn: tunnelApi.getAllTunnels,
refetchInterval: 5000,
});
const { data: tunnelStatuses } = useQuery({
queryKey: ['tunnel-statuses'],
queryFn: tunnelApi.getAllTunnelStatuses,
refetchInterval: 10000,
});
const { data: nodeConnection } = useQuery({
queryKey: ['node-connection'],
queryFn: nodeApi.getConnection,
refetchInterval: 30000,
retry: false,
});
const deleteMutation = useMutation({
mutationFn: tunnelApi.deleteTunnel,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['tunnels'] });
toast.success('Tunnel deleted successfully');
},
onError: (error) => {
console.error('Delete error:', error);
toast.error('Failed to delete tunnel');
},
});
const updateMutation = useMutation({
mutationFn: ({ id, updates }: { id: string; updates: Partial<TunnelConfig> }) =>
tunnelApi.updateTunnel(id, updates),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['tunnels'] });
toast.success('Tunnel updated successfully');
},
onError: (error) => {
console.error('Update error:', error);
toast.error('Failed to update tunnel');
},
});
const pushToNodeMutation = useMutation({
mutationFn: nodeApi.pushAndRestart,
onSuccess: (data) => {
toast.success(`Successfully pushed ${data.tunnelCount} tunnels to node and restarted frpc`);
queryClient.invalidateQueries({ queryKey: ['node-connection'] });
},
onError: (error: any) => {
console.error('Push to node error:', error);
toast.error(error.response?.data?.error || 'Failed to push configuration to node');
},
});
const handleDelete = (id: string) => {
if (window.confirm('Are you sure you want to delete this tunnel?')) {
deleteMutation.mutate(id);
}
};
const handleEdit = (tunnel: TunnelConfig) => {
setEditingTunnel(tunnel);
setShowForm(true);
};
const handleToggleEnabled = (tunnel: TunnelConfig) => {
updateMutation.mutate({
id: tunnel.id!,
updates: { enabled: !tunnel.enabled },
});
};
const handleFormClose = () => {
setShowForm(false);
setEditingTunnel(null);
};
const handleFormSubmit = () => {
handleFormClose();
queryClient.invalidateQueries({ queryKey: ['tunnels'] });
};
const handlePushToNode = () => {
if (window.confirm('Push current tunnel configuration to node and restart frpc?')) {
pushToNodeMutation.mutate();
}
};
if (isLoading) {
return (
<div className="tunnel-manager">
<div className="loading">Loading tunnels...</div>
</div>
);
}
return (
<div className="tunnel-manager">
<div className="tunnel-header">
<h1>Tunnel Manager</h1>
<div className="header-actions">
{nodeConnection && (
<div className="node-status">
<span className={`node-indicator ${nodeConnection.isReachable ? 'online' : 'offline'}`}>
Node: {nodeConnection.isReachable ? 'Online' : 'Offline'}
</span>
<button
className="btn btn-secondary"
onClick={handlePushToNode}
disabled={pushToNodeMutation.isPending || !nodeConnection.isReachable}
title="Push configuration to node and restart frpc"
>
{pushToNodeMutation.isPending ? (
<RefreshCw size={18} className="spinning" />
) : (
<Send size={18} />
)}
Push to Node
</button>
</div>
)}
<button
className="btn btn-primary"
onClick={() => setShowForm(true)}
>
<Plus size={18} />
Add Tunnel
</button>
</div>
</div>
<div className="tunnels-grid">
{tunnels?.map(tunnel => {
const status = tunnelStatuses?.find(s => s.id === tunnel.id);
return (
<div key={tunnel.id} className="tunnel-card">
<div className="tunnel-card-header">
<div className="tunnel-title">
<h3>{tunnel.name}</h3>
<div className="tunnel-badges">
<span className={`badge ${tunnel.enabled ? 'badge-success' : 'badge-secondary'}`}>
{tunnel.enabled ? 'Enabled' : 'Disabled'}
</span>
<span className={`badge ${status?.active ? 'badge-active' : 'badge-inactive'}`}>
{status?.active ? 'Active' : 'Inactive'}
</span>
</div>
</div>
<div className="tunnel-actions">
<button
className={`btn btn-sm ${tunnel.enabled ? 'btn-warning' : 'btn-success'}`}
onClick={() => handleToggleEnabled(tunnel)}
disabled={updateMutation.isPending}
>
{tunnel.enabled ? <PowerOff size={16} /> : <Power size={16} />}
</button>
<button
className="btn btn-sm btn-secondary"
onClick={() => handleEdit(tunnel)}
>
<Edit2 size={16} />
</button>
<button
className="btn btn-sm btn-danger"
onClick={() => handleDelete(tunnel.id!)}
disabled={deleteMutation.isPending}
>
<Trash2 size={16} />
</button>
</div>
</div>
<div className="tunnel-info">
<div className="info-row">
<span className="info-label">Protocol:</span>
<span className="info-value">{tunnel.protocol}</span>
</div>
<div className="info-row">
<span className="info-label">Local:</span>
<span className="info-value">{tunnel.localIp}:{tunnel.localPort}</span>
</div>
<div className="info-row">
<span className="info-label">Remote:</span>
<span className="info-value">:{tunnel.remotePort}</span>
</div>
<div className="info-row">
<span className="info-label">Created:</span>
<span className="info-value">
{tunnel.createdAt ? new Date(tunnel.createdAt).toLocaleDateString() : 'N/A'}
</span>
</div>
</div>
{status?.error && (
<div className="tunnel-error">
<span className="error-text">Error: {status.error}</span>
</div>
)}
</div>
);
})}
</div>
{tunnels?.length === 0 && (
<div className="empty-state">
<Settings size={64} className="empty-icon" />
<h3>No tunnels configured</h3>
<p>Get started by adding your first tunnel configuration</p>
<button
className="btn btn-primary"
onClick={() => setShowForm(true)}
>
<Plus size={18} />
Add Your First Tunnel
</button>
</div>
)}
{showForm && (
<TunnelForm
tunnel={editingTunnel}
onClose={handleFormClose}
onSubmit={handleFormSubmit}
/>
)}
</div>
);
};
export default TunnelManager;