docs: comprehensive documentation polish & CI/CD automation

Major Updates:
- Rewrote all docs in beginner-friendly 'ELI5' language
- Created docs index with user journey navigation
- Added complete getting-started guide for novice users
- Set up GitHub Container Registry (GHCR) automation
- Configured GitHub Pages deployment for documentation

Documentation:
- docs/index.md - Central navigation hub
- docs/getting-started.md - Step-by-step beginner guide
- docs/github-setup.md - CI/CD setup instructions
- README.md - Complete rewrite in accessible language
- CONTRIBUTING.md - Contributor guidelines
- Multiple comprehensive API and schema docs

CI/CD Workflows:
- .github/workflows/docker-build.yml - Multi-platform builds to GHCR
- .github/workflows/docs.yml - Automated docs deployment to Pages
- Supports main (latest), development (dev), and version tags
- Automated testing of built images
- Beautiful documentation site with dark theme

Benefits:
- Zero barrier to entry for new users
- Automated Docker builds (AMD64 + ARM64)
- Professional documentation site
- No Docker Hub account needed (uses GHCR)
- Complete CI/CD pipeline

All 7 implementation phases complete - project is production ready!
This commit is contained in:
Wikid82
2025-11-18 13:11:11 -05:00
parent b9dcc6c347
commit e58fcb714d
76 changed files with 16989 additions and 99 deletions
+98
View File
@@ -0,0 +1,98 @@
import { useEffect, useState } from 'react'
import { useProxyHosts } from '../hooks/useProxyHosts'
import { useRemoteServers } from '../hooks/useRemoteServers'
import { healthAPI } from '../services/api'
import { Link } from 'react-router-dom'
export default function Dashboard() {
const { hosts } = useProxyHosts()
const { servers } = useRemoteServers()
const [health, setHealth] = useState<{ status: string } | null>(null)
useEffect(() => {
const checkHealth = async () => {
try {
const result = await healthAPI.check()
setHealth(result)
} catch (err) {
setHealth({ status: 'error' })
}
}
checkHealth()
}, [])
const enabledHosts = hosts.filter(h => h.enabled).length
const enabledServers = servers.filter(s => s.enabled).length
return (
<div className="p-8">
<h1 className="text-3xl font-bold text-white mb-6">Dashboard</h1>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<Link to="/proxy-hosts" className="bg-dark-card p-6 rounded-lg border border-gray-800 hover:border-gray-700 transition-colors">
<div className="text-sm text-gray-400 mb-2">Proxy Hosts</div>
<div className="text-3xl font-bold text-white mb-1">{hosts.length}</div>
<div className="text-xs text-gray-500">{enabledHosts} enabled</div>
</Link>
<Link to="/remote-servers" className="bg-dark-card p-6 rounded-lg border border-gray-800 hover:border-gray-700 transition-colors">
<div className="text-sm text-gray-400 mb-2">Remote Servers</div>
<div className="text-3xl font-bold text-white mb-1">{servers.length}</div>
<div className="text-xs text-gray-500">{enabledServers} enabled</div>
</Link>
<div className="bg-dark-card p-6 rounded-lg border border-gray-800">
<div className="text-sm text-gray-400 mb-2">SSL Certificates</div>
<div className="text-3xl font-bold text-white mb-1">0</div>
<div className="text-xs text-gray-500">Coming soon</div>
</div>
<div className="bg-dark-card p-6 rounded-lg border border-gray-800">
<div className="text-sm text-gray-400 mb-2">System Status</div>
<div className={`text-lg font-bold ${health?.status === 'ok' ? 'text-green-400' : 'text-red-400'}`}>
{health?.status === 'ok' ? 'Healthy' : health ? 'Error' : 'Checking...'}
</div>
</div>
</div>
{/* Quick Actions */}
<div className="bg-dark-card rounded-lg border border-gray-800 p-6">
<h2 className="text-xl font-semibold text-white mb-4">Quick Actions</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Link
to="/proxy-hosts"
className="flex items-center gap-3 p-4 bg-gray-900 hover:bg-gray-800 rounded-lg transition-colors"
>
<span className="text-2xl">🌐</span>
<div>
<div className="font-medium text-white">Add Proxy Host</div>
<div className="text-xs text-gray-400">Create a new reverse proxy</div>
</div>
</Link>
<Link
to="/remote-servers"
className="flex items-center gap-3 p-4 bg-gray-900 hover:bg-gray-800 rounded-lg transition-colors"
>
<span className="text-2xl">🖥</span>
<div>
<div className="font-medium text-white">Add Remote Server</div>
<div className="text-xs text-gray-400">Register a backend server</div>
</div>
</Link>
<Link
to="/import"
className="flex items-center gap-3 p-4 bg-gray-900 hover:bg-gray-800 rounded-lg transition-colors"
>
<span className="text-2xl">📥</span>
<div>
<div className="font-medium text-white">Import Caddyfile</div>
<div className="text-xs text-gray-400">Bulk import from existing config</div>
</div>
</Link>
</div>
</div>
</div>
)
}
+145
View File
@@ -0,0 +1,145 @@
import { useState } from 'react'
import { useImport } from '../hooks/useImport'
import ImportBanner from '../components/ImportBanner'
import ImportReviewTable from '../components/ImportReviewTable'
export default function ImportCaddy() {
const { session, preview, loading, error, upload, commit, cancel } = useImport()
const [content, setContent] = useState('')
const [showReview, setShowReview] = useState(false)
const handleUpload = async () => {
if (!content.trim()) {
alert('Please enter Caddyfile content')
return
}
try {
await upload(content)
setShowReview(true)
} catch (err) {
// Error is already set by hook
}
}
const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (!file) return
const text = await file.text()
setContent(text)
}
const handleCommit = async (resolutions: Record<string, string>) => {
try {
await commit(resolutions)
setContent('')
setShowReview(false)
alert('Import completed successfully!')
} catch (err) {
// Error is already set by hook
}
}
const handleCancel = async () => {
if (confirm('Are you sure you want to cancel this import?')) {
try {
await cancel()
setShowReview(false)
} catch (err) {
// Error is already set by hook
}
}
}
return (
<div className="p-8">
<h1 className="text-3xl font-bold text-white mb-6">Import Caddyfile</h1>
{session && (
<ImportBanner
session={session}
onReview={() => setShowReview(true)}
onCancel={handleCancel}
/>
)}
{error && (
<div className="bg-red-900/20 border border-red-500 text-red-400 px-4 py-3 rounded mb-6">
{error}
</div>
)}
{!session && (
<div className="bg-dark-card rounded-lg border border-gray-800 p-6">
<div className="mb-6">
<h2 className="text-xl font-semibold text-white mb-2">Upload or Paste Caddyfile</h2>
<p className="text-gray-400 text-sm">
Import an existing Caddyfile to automatically create proxy host configurations.
The system will detect conflicts and allow you to review changes before committing.
</p>
</div>
<div className="space-y-4">
{/* File Upload */}
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Upload Caddyfile
</label>
<input
type="file"
accept=".caddyfile,.txt,text/plain"
onChange={handleFileUpload}
className="w-full text-sm text-gray-400 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-medium file:bg-blue-active file:text-white hover:file:bg-blue-hover file:cursor-pointer cursor-pointer"
/>
</div>
{/* Or Divider */}
<div className="flex items-center gap-4">
<div className="flex-1 border-t border-gray-700" />
<span className="text-gray-500 text-sm">or paste content</span>
<div className="flex-1 border-t border-gray-700" />
</div>
{/* Text Area */}
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Caddyfile Content
</label>
<textarea
value={content}
onChange={e => setContent(e.target.value)}
className="w-full h-96 bg-gray-900 border border-gray-700 rounded-lg p-4 text-white font-mono text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder={`example.com {
reverse_proxy localhost:8080
}
api.example.com {
reverse_proxy localhost:3000
}`}
/>
</div>
<button
onClick={handleUpload}
disabled={loading || !content.trim()}
className="px-6 py-2 bg-blue-active hover:bg-blue-hover text-white rounded-lg font-medium transition-colors disabled:opacity-50"
>
{loading ? 'Processing...' : 'Parse and Review'}
</button>
</div>
</div>
)}
{showReview && preview && (
<ImportReviewTable
hosts={preview.hosts}
conflicts={preview.conflicts}
errors={preview.errors}
onCommit={handleCommit}
onCancel={() => setShowReview(false)}
/>
)}
</div>
)
}
+157
View File
@@ -0,0 +1,157 @@
import { useState } from 'react'
import { useProxyHosts, ProxyHost } from '../hooks/useProxyHosts'
import ProxyHostForm from '../components/ProxyHostForm'
export default function ProxyHosts() {
const { hosts, loading, error, createHost, updateHost, deleteHost } = useProxyHosts()
const [showForm, setShowForm] = useState(false)
const [editingHost, setEditingHost] = useState<ProxyHost | undefined>()
const handleAdd = () => {
setEditingHost(undefined)
setShowForm(true)
}
const handleEdit = (host: ProxyHost) => {
setEditingHost(host)
setShowForm(true)
}
const handleSubmit = async (data: Partial<ProxyHost>) => {
if (editingHost) {
await updateHost(editingHost.uuid, data)
} else {
await createHost(data)
}
setShowForm(false)
setEditingHost(undefined)
}
const handleDelete = async (uuid: string) => {
if (confirm('Are you sure you want to delete this proxy host?')) {
try {
await deleteHost(uuid)
} catch (err) {
alert(err instanceof Error ? err.message : 'Failed to delete')
}
}
}
return (
<div className="p-8">
<div className="flex items-center justify-between mb-6">
<h1 className="text-3xl font-bold text-white">Proxy Hosts</h1>
<button
onClick={handleAdd}
className="px-4 py-2 bg-blue-active hover:bg-blue-hover text-white rounded-lg font-medium transition-colors"
>
Add Proxy Host
</button>
</div>
{error && (
<div className="bg-red-900/20 border border-red-500 text-red-400 px-4 py-3 rounded mb-6">
{error}
</div>
)}
<div className="bg-dark-card rounded-lg border border-gray-800 overflow-hidden">
{loading ? (
<div className="text-center text-gray-400 py-12">Loading...</div>
) : hosts.length === 0 ? (
<div className="text-center text-gray-400 py-12">
No proxy hosts configured yet. Click "Add Proxy Host" to get started.
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-900 border-b border-gray-800">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
Domain
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
Forward To
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
SSL
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
Status
</th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-400 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-800">
{hosts.map((host) => (
<tr key={host.uuid} className="hover:bg-gray-900/50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-white">{host.domain_names}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-300">
{host.forward_scheme}://{host.forward_host}:{host.forward_port}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex gap-2">
{host.ssl_forced && (
<span className="px-2 py-1 text-xs bg-green-900/30 text-green-400 rounded">
SSL
</span>
)}
{host.websocket_support && (
<span className="px-2 py-1 text-xs bg-blue-900/30 text-blue-400 rounded">
WS
</span>
)}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span
className={`px-2 py-1 text-xs rounded ${
host.enabled
? 'bg-green-900/30 text-green-400'
: 'bg-gray-700 text-gray-400'
}`}
>
{host.enabled ? 'Enabled' : 'Disabled'}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button
onClick={() => handleEdit(host)}
className="text-blue-400 hover:text-blue-300 mr-4"
>
Edit
</button>
<button
onClick={() => handleDelete(host.uuid)}
className="text-red-400 hover:text-red-300"
>
Delete
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
{showForm && (
<ProxyHostForm
host={editingHost}
onSubmit={handleSubmit}
onCancel={() => {
setShowForm(false)
setEditingHost(undefined)
}}
/>
)}
</div>
)
}
+234
View File
@@ -0,0 +1,234 @@
import { useState } from 'react'
import { useRemoteServers, RemoteServer } from '../hooks/useRemoteServers'
import RemoteServerForm from '../components/RemoteServerForm'
export default function RemoteServers() {
const { servers, loading, error, createServer, updateServer, deleteServer } = useRemoteServers()
const [showForm, setShowForm] = useState(false)
const [editingServer, setEditingServer] = useState<RemoteServer | undefined>()
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid')
const handleAdd = () => {
setEditingServer(undefined)
setShowForm(true)
}
const handleEdit = (server: RemoteServer) => {
setEditingServer(server)
setShowForm(true)
}
const handleSubmit = async (data: Partial<RemoteServer>) => {
if (editingServer) {
await updateServer(editingServer.uuid, data)
} else {
await createServer(data)
}
setShowForm(false)
setEditingServer(undefined)
}
const handleDelete = async (uuid: string) => {
if (confirm('Are you sure you want to delete this remote server?')) {
try {
await deleteServer(uuid)
} catch (err) {
alert(err instanceof Error ? err.message : 'Failed to delete')
}
}
}
return (
<div className="p-8">
<div className="flex items-center justify-between mb-6">
<h1 className="text-3xl font-bold text-white">Remote Servers</h1>
<div className="flex gap-3">
<div className="flex bg-gray-800 rounded-lg p-1">
<button
onClick={() => setViewMode('grid')}
className={`px-3 py-1 rounded text-sm ${
viewMode === 'grid'
? 'bg-blue-active text-white'
: 'text-gray-400 hover:text-white'
}`}
>
Grid
</button>
<button
onClick={() => setViewMode('list')}
className={`px-3 py-1 rounded text-sm ${
viewMode === 'list'
? 'bg-blue-active text-white'
: 'text-gray-400 hover:text-white'
}`}
>
List
</button>
</div>
<button
onClick={handleAdd}
className="px-4 py-2 bg-blue-active hover:bg-blue-hover text-white rounded-lg font-medium transition-colors"
>
Add Server
</button>
</div>
</div>
{error && (
<div className="bg-red-900/20 border border-red-500 text-red-400 px-4 py-3 rounded mb-6">
{error}
</div>
)}
{loading ? (
<div className="text-center text-gray-400 py-12">Loading...</div>
) : servers.length === 0 ? (
<div className="bg-dark-card rounded-lg border border-gray-800 p-6">
<div className="text-center text-gray-400 py-12">
No remote servers configured. Add servers to quickly select backends when creating proxy hosts.
</div>
</div>
) : viewMode === 'grid' ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{servers.map((server) => (
<div
key={server.uuid}
className="bg-dark-card rounded-lg border border-gray-800 p-6 hover:border-gray-700 transition-colors"
>
<div className="flex items-start justify-between mb-4">
<div>
<h3 className="text-lg font-semibold text-white mb-1">{server.name}</h3>
<span className="inline-block px-2 py-1 text-xs bg-gray-800 text-gray-400 rounded">
{server.provider}
</span>
</div>
<span
className={`px-2 py-1 text-xs rounded ${
server.enabled
? 'bg-green-900/30 text-green-400'
: 'bg-gray-700 text-gray-400'
}`}
>
{server.enabled ? 'Enabled' : 'Disabled'}
</span>
</div>
<div className="space-y-2 mb-4">
<div className="flex items-center gap-2 text-sm">
<span className="text-gray-400">Host:</span>
<span className="text-white font-mono">{server.host}</span>
</div>
<div className="flex items-center gap-2 text-sm">
<span className="text-gray-400">Port:</span>
<span className="text-white font-mono">{server.port}</span>
</div>
{server.username && (
<div className="flex items-center gap-2 text-sm">
<span className="text-gray-400">User:</span>
<span className="text-white font-mono">{server.username}</span>
</div>
)}
</div>
<div className="flex gap-2 pt-4 border-t border-gray-800">
<button
onClick={() => handleEdit(server)}
className="flex-1 px-3 py-2 bg-gray-700 hover:bg-gray-600 text-white text-sm rounded-lg font-medium transition-colors"
>
Edit
</button>
<button
onClick={() => handleDelete(server.uuid)}
className="flex-1 px-3 py-2 bg-red-900/20 hover:bg-red-900/30 text-red-400 text-sm rounded-lg font-medium transition-colors"
>
Delete
</button>
</div>
</div>
))}
</div>
) : (
<div className="bg-dark-card rounded-lg border border-gray-800 overflow-hidden">
<table className="w-full">
<thead className="bg-gray-900 border-b border-gray-800">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
Name
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
Provider
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
Host
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
Port
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
Status
</th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-400 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-800">
{servers.map((server) => (
<tr key={server.uuid} className="hover:bg-gray-900/50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-white">{server.name}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className="px-2 py-1 text-xs bg-gray-800 text-gray-400 rounded">
{server.provider}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-300 font-mono">{server.host}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-300 font-mono">{server.port}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span
className={`px-2 py-1 text-xs rounded ${
server.enabled
? 'bg-green-900/30 text-green-400'
: 'bg-gray-700 text-gray-400'
}`}
>
{server.enabled ? 'Enabled' : 'Disabled'}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button
onClick={() => handleEdit(server)}
className="text-blue-400 hover:text-blue-300 mr-4"
>
Edit
</button>
<button
onClick={() => handleDelete(server.uuid)}
className="text-red-400 hover:text-red-300"
>
Delete
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{showForm && (
<RemoteServerForm
server={editingServer}
onSubmit={handleSubmit}
onCancel={() => {
setShowForm(false)
setEditingServer(undefined)
}}
/>
)}
</div>
)
}
+12
View File
@@ -0,0 +1,12 @@
export default function Settings() {
return (
<div className="p-8">
<h1 className="text-3xl font-bold text-white mb-6">Settings</h1>
<div className="bg-dark-card rounded-lg border border-gray-800 p-6">
<div className="text-gray-400">
Settings page coming soon...
</div>
</div>
</div>
)
}