Compare commits

..

No commits in common. "c1dc53199593af0d975e86a51898d55ca8ae142f" and "143dac75ba2794eb23bbe8a35708a9c9b62b641c" have entirely different histories.

6 changed files with 795 additions and 1062 deletions

View File

@ -1,55 +0,0 @@
name: Build and Push Svelte Docker Image
on:
push:
branches: [prod]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build Svelte app
run: npm run build
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Gitea Container Registry
uses: docker/login-action@v2
with:
registry: git.vinylnostalgia.com
username: ${{ secrets.RUNNER_USERNAME }}
password: ${{ secrets.RUNNER_PASSWORD }}
- name: Generate Docker metadata
id: meta
uses: docker/metadata-action@v4
with:
images: git.vinylnostalgia.com/mo/caddyui
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@ -1,11 +0,0 @@
# Use a minimal base image to serve static files
FROM nginx:alpine
# Copy the built files from the local system to the nginx web root
COPY build /usr/share/nginx/html
# Expose port 80 for serving the application
EXPOSE 80
# Start Nginx
CMD ["nginx", "-g", "daemon off;"]

1562
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -17,7 +17,6 @@
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.28.1", "@playwright/test": "^1.28.1",
"@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/adapter-node": "^5.2.11",
"@sveltejs/kit": "^2.0.0", "@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0",
"@tailwindcss/typography": "^0.5.14", "@tailwindcss/typography": "^0.5.14",

View File

@ -17,216 +17,211 @@
Textarea, Textarea,
Spinner Spinner
} from 'flowbite-svelte'; } from 'flowbite-svelte';
import { fly, fade } from 'svelte/transition';
import type { Upstream, CAInfo } from './CaddyService'; import type { Upstream, CAInfo } from './CaddyService';
// State variables let config: Record<string, any> = {};
let config = {};
let upstreams: Upstream[] = []; let upstreams: Upstream[] = [];
let caInfo: CAInfo | null = null; let caInfo: CAInfo | null = null;
let caCertificates = ''; let caCertificates: string = '';
let showConfigModal = false; let showConfigModal: boolean = false;
let showCAModal = false; let showCAModal: boolean = false;
let loadingConfig = false; let loadingConfig: boolean = false;
let loadingUpstreams = false; let loadingUpstreams: boolean = false;
let newConfig = ''; let newConfig: string = '';
let configPath = ''; let configPath: string = '';
let caId = 'local'; let caId: string = 'local';
let activeTab = 'config'; // Tracks the active tab let activeTab: string = 'config';
// Subscribe to stores configStore.subscribe((value) => (config = value));
configStore.subscribe((value: any) => (config = value)); upstreamsStore.subscribe((value) => (upstreams = value));
upstreamsStore.subscribe((value: any) => (upstreams = value));
onMount(async () => { onMount(async () => {
await refreshData(); await refreshData();
}); });
// Fetch and refresh data async function refreshData(): Promise<void> {
async function refreshData() {
loadingConfig = true;
try { try {
loadingConfig = true;
await CaddyService.getConfig(); await CaddyService.getConfig();
await CaddyService.getUpstreams(); await CaddyService.getUpstreams();
} catch (error) { } catch (error) {
alert(`Error refreshing data: ${(error as Error).message}`); alert(`Failed to refresh data: ${(error as Error).message}`);
} finally { } finally {
loadingConfig = false; loadingConfig = false;
loadingUpstreams = false; loadingUpstreams = false;
} }
} }
// Update configuration async function handleUpdateConfig(): Promise<void> {
async function handleUpdateConfig() {
try { try {
loadingConfig = true; loadingConfig = true;
await CaddyService.updateConfig(configPath, JSON.parse(newConfig)); await CaddyService.updateConfig(configPath, JSON.parse(newConfig));
await refreshData(); await refreshData();
showConfigModal = false; showConfigModal = false;
} catch (error) { } catch (error) {
alert(`Error updating config: ${(error as Error).message}`); alert(`Failed to update config: ${(error as Error).message}`);
} finally { } finally {
loadingConfig = false; loadingConfig = false;
} }
} }
// Load new configuration async function handleLoadConfig(): Promise<void> {
async function handleLoadConfig() {
try { try {
loadingConfig = true; loadingConfig = true;
await CaddyService.loadConfig(JSON.parse(newConfig)); await CaddyService.loadConfig(JSON.parse(newConfig));
await refreshData(); await refreshData();
showConfigModal = false; showConfigModal = false;
} catch (error) { } catch (error) {
alert(`Error loading config: ${(error as Error).message}`); alert(`Failed to load config: ${(error as Error).message}`);
} finally { } finally {
loadingConfig = false; loadingConfig = false;
} }
} }
// Stop the Caddy server async function handleStopServer(): Promise<void> {
async function handleStopServer() {
if (confirm('Are you sure you want to stop the Caddy server?')) { if (confirm('Are you sure you want to stop the Caddy server?')) {
try { try {
await CaddyService.stopServer(); await CaddyService.stopServer();
alert('Caddy server stopped successfully'); alert('Caddy server stopped successfully');
} catch (error) { } catch (error) {
alert(`Error stopping server: ${(error as Error).message}`); alert(`Failed to stop server: ${(error as Error).message}`);
} }
} }
} }
// Fetch CA information async function handleGetCAInfo(): Promise<void> {
async function handleGetCAInfo() {
try { try {
caInfo = await CaddyService.getCAInfo(caId); caInfo = await CaddyService.getCAInfo(caId);
caCertificates = await CaddyService.getCACertificates(caId); caCertificates = await CaddyService.getCACertificates(caId);
showCAModal = true; showCAModal = true;
} catch (error) { } catch (error) {
alert(`Error fetching CA info: ${(error as Error).message}`); alert(`Failed to get CA info: ${(error as Error).message}`);
} }
} }
// Change active tab
function handleTabChange(tab: string) { function handleTabChange(tab: string) {
activeTab = tab; activeTab = tab;
} }
import { Cog, ServerCrash, Shield, RefreshCw } from 'lucide-svelte';
</script> </script>
<main class="container mx-auto min-h-screen bg-gray-50 p-6"> <main class="container mx-auto p-4 bg-gray-50 min-h-screen">
<header class="mb-6"> <h1 class="text-4xl font-bold mb-6 text-caddy-green">Caddy UI</h1>
<h1 class="text-caddy-green text-4xl font-bold">Caddy Dashboard</h1>
<p class="mt-2 text-gray-700">
Manage your Caddy server configurations, upstreams, and CA information easily.
</p>
</header>
<Tabs style="pills"> <Tabs style="pills">
<!-- Removed bind:active -->
<TabItem <TabItem
open={activeTab === 'config'} open={activeTab === 'config'}
on:click={() => handleTabChange('config')} on:click={() => handleTabChange('config')}
title="Configuration" title="Configuration"
/> class="flex items-center gap-2"
></TabItem>
<TabItem <TabItem
open={activeTab === 'upstreams'} open={activeTab === 'upstreams'}
on:click={() => handleTabChange('upstreams')} on:click={() => handleTabChange('upstreams')}
title="Upstreams" title="Upstreams"
/> class="flex items-center gap-2"
></TabItem>
<TabItem <TabItem
open={activeTab === 'ca_management'} open={activeTab === 'ca_management'}
on:click={() => handleTabChange('ca_management')} on:click={() => handleTabChange('ca_management')}
title="CA Management" title="CA Management"
/> class="flex items-center gap-2"
></TabItem>
</Tabs> </Tabs>
<!-- Tab Content --> <!-- Display content based on the activeTab -->
{#if activeTab === 'config'} {#if activeTab === 'config'}
<section class="mt-6"> <div class="mt-6">
<Card class="shadow-lg"> <Card class="shadow-lg">
<header class="mb-4 flex items-center justify-between"> <div class="flex justify-between items-center mb-4">
<h2 class="text-caddy-blue text-2xl font-semibold">Current Configuration</h2> <h2 class="text-2xl font-semibold text-caddy-blue">Current Configuration</h2>
<Button on:click={() => (showConfigModal = true)} class="flex items-center gap-2"> <Button
color="green"
on:click={() => (showConfigModal = true)}
class="flex items-center gap-2"
>
<RefreshCw size="16" />
Update Configuration Update Configuration
</Button> </Button>
</header> </div>
{#if loadingConfig} {#if loadingConfig}
<div class="flex justify-center p-4"> <div class="flex justify-center p-4">
<Spinner size="xl" color="blue" /> <Spinner size="xl" color="blue" />
</div> </div>
{:else} {:else}
<pre class="overflow-x-auto rounded-lg bg-gray-100 p-4 text-sm"> <pre class="bg-gray-100 p-4 rounded-lg overflow-x-auto text-sm">{JSON.stringify(
{JSON.stringify(config, null, 2)} config,
</pre> null,
2
)}</pre>
{/if} {/if}
</Card> </Card>
</section> </div>
{/if} {/if}
{#if activeTab === 'upstreams'} {#if activeTab === 'upstreams'}
<section class="mt-6"> <div class="mt-6">
<Card class="shadow-lg"> <Card class="shadow-lg">
<header class="mb-4"> <h2 class="text-2xl font-semibold mb-4 text-caddy-blue">Upstreams</h2>
<h2 class="text-caddy-blue text-2xl font-semibold">Upstreams</h2>
</header>
{#if loadingUpstreams} {#if loadingUpstreams}
<div class="flex justify-center p-4"> <div class="flex justify-center p-4">
<Spinner size="xl" color="blue" /> <Spinner size="xl" color="blue" />
</div> </div>
{:else} {:else}
<Table striped hoverable> <div class="overflow-x-auto">
<TableHead> <Table striped={true} hoverable={true}>
<TableHeadCell>Address</TableHeadCell> <TableHead>
<TableHeadCell>Requests</TableHeadCell> <TableHeadCell>Address</TableHeadCell>
<TableHeadCell>Fails</TableHeadCell> <TableHeadCell>Requests</TableHeadCell>
</TableHead> <TableHeadCell>Fails</TableHeadCell>
<TableBody> </TableHead>
{#each upstreams as upstream} <TableBody>
<TableBodyRow> {#each upstreams as upstream}
<TableBodyCell>{upstream.address}</TableBodyCell> <TableBodyRow>
<TableBodyCell>{upstream.num_requests}</TableBodyCell> <TableBodyCell>{upstream.address}</TableBodyCell>
<TableBodyCell>{upstream.fails}</TableBodyCell> <TableBodyCell>{upstream.num_requests}</TableBodyCell>
</TableBodyRow> <TableBodyCell>{upstream.fails}</TableBodyCell>
{/each} </TableBodyRow>
</TableBody> {/each}
</Table> </TableBody>
</Table>
</div>
{/if} {/if}
</Card> </Card>
</section> </div>
{/if} {/if}
{#if activeTab === 'ca_management'} {#if activeTab === 'ca_management'}
<section class="mt-6"> <div class="mt-6">
<Card class="shadow-lg"> <Card class="shadow-lg">
<header class="mb-4"> <h2 class="text-2xl font-semibold mb-4 text-caddy-blue">CA Management</h2>
<h2 class="text-caddy-blue text-2xl font-semibold">CA Management</h2> <div class="flex items-center gap-2">
</header> <Input type="text" placeholder="CA ID (e.g., local)" bind:value={caId} />
<div class="flex items-center gap-4"> <Button color="green" on:click={handleGetCAInfo}>Get CA Info</Button>
<Input
type="text"
placeholder="Enter CA ID (e.g., local)"
bind:value={caId}
class="flex-1"
/>
<Button on:click={handleGetCAInfo}>Get CA Info</Button>
</div> </div>
</Card> </Card>
</section> </div>
{/if} {/if}
<footer class="mt-6 flex justify-end">
<Button color="red" on:click={handleStopServer} class="flex items-center gap-2">
Stop Caddy Server
</Button>
</footer>
</main> </main>
<!-- Modals --> <div class="mt-6 flex justify-end">
<Modal bind:open={showConfigModal} size="xl" autoclose={false}> <Button color="red" on:click={handleStopServer} class="flex items-center gap-2">
<h2 class="text-caddy-blue mb-4 text-2xl font-semibold">Update Configuration</h2> <ServerCrash size="16" />
Stop Caddy Server
</Button>
</div>
<Modal bind:open={showConfigModal} size="xl" autoclose={false} class="w-full max-w-4xl">
<h2 class="text-2xl font-semibold mb-4 text-caddy-blue">Update Configuration</h2>
<Input <Input
type="text" type="text"
placeholder="Config path (e.g., apps/http/servers/myserver/listen)" placeholder="Config path (e.g., apps/http/servers/myserver/listen)"
bind:value={configPath}
class="mb-4" class="mb-4"
bind:value={configPath}
/> />
<Textarea <Textarea
rows={10} rows={10}
@ -234,29 +229,32 @@
bind:value={newConfig} bind:value={newConfig}
class="mb-4" class="mb-4"
/> />
<div class="flex justify-end gap-4"> <div class="flex justify-end gap-2">
<Button on:click={() => (showConfigModal = false)}>Cancel</Button> <Button color="light" on:click={() => (showConfigModal = false)}>Cancel</Button>
<Button color="green" on:click={handleUpdateConfig}>Update</Button> <Button color="green" on:click={handleUpdateConfig}>Update Config</Button>
<Button color="blue" on:click={handleLoadConfig}>Load Full Config</Button> <Button color="blue" on:click={handleLoadConfig}>Load Full Config</Button>
</div> </div>
</Modal> </Modal>
<Modal bind:open={showCAModal} size="xl" autoclose={false}> <Modal bind:open={showCAModal} size="xl" autoclose={false}>
<h2 class="text-caddy-blue mb-4 text-2xl font-semibold">CA Information</h2> <h2 class="text-2xl font-semibold mb-4 text-caddy-blue">CA Information</h2>
{#if caInfo} {#if caInfo}
<div class="space-y-4"> <div class="mb-4 space-y-2">
<p><strong>ID:</strong> {caInfo.id}</p> <p><strong class="text-caddy-green">ID:</strong> {caInfo.id}</p>
<p><strong>Name:</strong> {caInfo.name}</p> <p><strong class="text-caddy-green">Name:</strong> {caInfo.name}</p>
<p><strong>Root CN:</strong> {caInfo.root_common_name}</p> <p><strong class="text-caddy-green">Root CN:</strong> {caInfo.root_common_name}</p>
<p><strong>Intermediate CN:</strong> {caInfo.intermediate_common_name}</p> <p>
<strong class="text-caddy-green">Intermediate CN:</strong>
{caInfo.intermediate_common_name}
</p>
</div> </div>
<h3 class="text-caddy-blue mt-4 text-lg font-semibold">Certificates</h3> <h3 class="text-lg font-semibold mb-2 text-caddy-blue">Certificates</h3>
<Textarea readonly rows={10} value={caCertificates} /> <Textarea rows={10} readonly value={caCertificates} class="mb-4" />
{:else} {:else}
<p>No CA information available.</p> <p class="text-gray-600">No CA information available.</p>
{/if} {/if}
<div class="mt-4 flex justify-end"> <div class="flex justify-end">
<Button on:click={() => (showCAModal = false)}>Close</Button> <Button color="blue" on:click={() => (showCAModal = false)}>Close</Button>
</div> </div>
</Modal> </Modal>
@ -264,10 +262,20 @@
:global(body) { :global(body) {
background-color: #f3f4f6; background-color: #f3f4f6;
} }
:global(.text-caddy-green) { :global(.text-caddy-green) {
color: #00add8; color: #00add8;
} }
:global(.text-caddy-blue) { :global(.text-caddy-blue) {
color: #0097b7; color: #0097b7;
} }
:global(.bg-caddy-green) {
background-color: #00add8;
}
:global(.bg-caddy-blue) {
background-color: #0097b7;
}
</style> </style>

View File

@ -1,4 +1,4 @@
import adapter from '@sveltejs/adapter-node'; import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */ /** @type {import('@sveltejs/kit').Config} */