initial commit

This commit is contained in:
2025-11-21 17:31:18 -06:00
commit 87cb54a17d
2451 changed files with 508075 additions and 0 deletions

View File

@@ -0,0 +1,364 @@
import { useState, useEffect } from 'react';
import { admin } from '../utils/api';
export default function AdminPanel() {
const [activeSection, setActiveSection] = useState('tokens');
const [tokens, setTokens] = useState([]);
const [runners, setRunners] = useState([]);
const [loading, setLoading] = useState(false);
const [newTokenExpires, setNewTokenExpires] = useState(24);
const [newToken, setNewToken] = useState(null);
useEffect(() => {
if (activeSection === 'tokens') {
loadTokens();
} else if (activeSection === 'runners') {
loadRunners();
}
}, [activeSection]);
const loadTokens = async () => {
setLoading(true);
try {
const data = await admin.listTokens();
setTokens(data);
} catch (error) {
console.error('Failed to load tokens:', error);
alert('Failed to load tokens');
} finally {
setLoading(false);
}
};
const loadRunners = async () => {
setLoading(true);
try {
const data = await admin.listRunners();
setRunners(data);
} catch (error) {
console.error('Failed to load runners:', error);
alert('Failed to load runners');
} finally {
setLoading(false);
}
};
const generateToken = async () => {
setLoading(true);
try {
const data = await admin.generateToken(newTokenExpires);
setNewToken(data.token);
await loadTokens();
} catch (error) {
console.error('Failed to generate token:', error);
alert('Failed to generate token');
} finally {
setLoading(false);
}
};
const revokeToken = async (tokenId) => {
if (!confirm('Are you sure you want to revoke this token?')) {
return;
}
try {
await admin.revokeToken(tokenId);
await loadTokens();
} catch (error) {
console.error('Failed to revoke token:', error);
alert('Failed to revoke token');
}
};
const verifyRunner = async (runnerId) => {
try {
await admin.verifyRunner(runnerId);
await loadRunners();
alert('Runner verified');
} catch (error) {
console.error('Failed to verify runner:', error);
alert('Failed to verify runner');
}
};
const deleteRunner = async (runnerId) => {
if (!confirm('Are you sure you want to delete this runner?')) {
return;
}
try {
await admin.deleteRunner(runnerId);
await loadRunners();
} catch (error) {
console.error('Failed to delete runner:', error);
alert('Failed to delete runner');
}
};
const copyToClipboard = (text) => {
navigator.clipboard.writeText(text);
alert('Copied to clipboard!');
};
const isTokenExpired = (expiresAt) => {
return new Date(expiresAt) < new Date();
};
const isTokenUsed = (used) => {
return used;
};
return (
<div className="space-y-6">
<div className="flex space-x-4 border-b border-gray-200">
<button
onClick={() => setActiveSection('tokens')}
className={`py-2 px-4 border-b-2 font-medium ${
activeSection === 'tokens'
? 'border-purple-600 text-purple-600'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`}
>
Registration Tokens
</button>
<button
onClick={() => setActiveSection('runners')}
className={`py-2 px-4 border-b-2 font-medium ${
activeSection === 'runners'
? 'border-purple-600 text-purple-600'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`}
>
Runner Management
</button>
</div>
{activeSection === 'tokens' && (
<div className="space-y-6">
<div className="bg-white rounded-lg shadow-md p-6">
<h2 className="text-xl font-semibold mb-4">Generate Registration Token</h2>
<div className="flex gap-4 items-end">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Expires in (hours)
</label>
<input
type="number"
min="1"
max="168"
value={newTokenExpires}
onChange={(e) => setNewTokenExpires(parseInt(e.target.value) || 24)}
className="w-32 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-600 focus:border-transparent"
/>
</div>
<button
onClick={generateToken}
disabled={loading}
className="px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
Generate Token
</button>
</div>
{newToken && (
<div className="mt-4 p-4 bg-green-50 border border-green-200 rounded-lg">
<p className="text-sm font-medium text-green-800 mb-2">New Token Generated:</p>
<div className="flex items-center gap-2">
<code className="flex-1 px-3 py-2 bg-white border border-green-300 rounded text-sm font-mono break-all">
{newToken}
</code>
<button
onClick={() => copyToClipboard(newToken)}
className="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 transition-colors text-sm"
>
Copy
</button>
</div>
<p className="text-xs text-green-700 mt-2">
Save this token securely. It will not be shown again.
</p>
</div>
)}
</div>
<div className="bg-white rounded-lg shadow-md p-6">
<h2 className="text-xl font-semibold mb-4">Active Tokens</h2>
{loading ? (
<div className="flex justify-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600"></div>
</div>
) : tokens.length === 0 ? (
<p className="text-gray-500 text-center py-8">No tokens generated yet.</p>
) : (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Token
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Expires At
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Created At
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{tokens.map((token) => {
const expired = isTokenExpired(token.expires_at);
const used = isTokenUsed(token.used);
return (
<tr key={token.id}>
<td className="px-6 py-4 whitespace-nowrap">
<code className="text-sm font-mono text-gray-900">
{token.token.substring(0, 16)}...
</code>
</td>
<td className="px-6 py-4 whitespace-nowrap">
{expired ? (
<span className="px-2 py-1 text-xs font-medium rounded-full bg-red-100 text-red-800">
Expired
</span>
) : used ? (
<span className="px-2 py-1 text-xs font-medium rounded-full bg-yellow-100 text-yellow-800">
Used
</span>
) : (
<span className="px-2 py-1 text-xs font-medium rounded-full bg-green-100 text-green-800">
Active
</span>
)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{new Date(token.expires_at).toLocaleString()}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{new Date(token.created_at).toLocaleString()}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm">
{!used && !expired && (
<button
onClick={() => revokeToken(token.id)}
className="text-red-600 hover:text-red-800 font-medium"
>
Revoke
</button>
)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</div>
</div>
)}
{activeSection === 'runners' && (
<div className="bg-white rounded-lg shadow-md p-6">
<h2 className="text-xl font-semibold mb-4">Runner Management</h2>
{loading ? (
<div className="flex justify-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600"></div>
</div>
) : runners.length === 0 ? (
<p className="text-gray-500 text-center py-8">No runners registered.</p>
) : (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Name
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Hostname
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Verified
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Last Heartbeat
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{runners.map((runner) => {
const isOnline = new Date(runner.last_heartbeat) > new Date(Date.now() - 60000);
return (
<tr key={runner.id}>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
{runner.name}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{runner.hostname}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span
className={`px-2 py-1 text-xs font-medium rounded-full ${
isOnline
? 'bg-green-100 text-green-800'
: 'bg-gray-100 text-gray-800'
}`}
>
{isOnline ? 'Online' : 'Offline'}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span
className={`px-2 py-1 text-xs font-medium rounded-full ${
runner.verified
? 'bg-green-100 text-green-800'
: 'bg-yellow-100 text-yellow-800'
}`}
>
{runner.verified ? 'Verified' : 'Unverified'}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{new Date(runner.last_heartbeat).toLocaleString()}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm space-x-2">
{!runner.verified && (
<button
onClick={() => verifyRunner(runner.id)}
className="text-blue-600 hover:text-blue-800 font-medium"
>
Verify
</button>
)}
<button
onClick={() => deleteRunner(runner.id)}
className="text-red-600 hover:text-red-800 font-medium"
>
Delete
</button>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</div>
)}
</div>
);
}