4d941f566f
Add app CRUD API endpoints, healthcheck service with node-cron scheduler, icon resolver (Lucide, Simple Icons, CDN, uploads), app management UI with Superforms, health badge component, and Docker health endpoint.
231 lines
6.7 KiB
Svelte
231 lines
6.7 KiB
Svelte
<script lang="ts">
|
|
import { superForm, type SuperValidated } from 'sveltekit-superforms';
|
|
import type { z } from 'zod';
|
|
import type { createAppSchema } from '$lib/utils/validators.js';
|
|
import AppIconPicker from './AppIconPicker.svelte';
|
|
|
|
type AppSchema = z.infer<typeof createAppSchema>;
|
|
|
|
interface Props {
|
|
form: SuperValidated<AppSchema>;
|
|
action?: string;
|
|
}
|
|
|
|
let { form: formData, action = '?/create' }: Props = $props();
|
|
|
|
const { form, errors, enhance, submitting } = superForm(formData, {
|
|
resetForm: true
|
|
});
|
|
|
|
let showAdvanced = $state(false);
|
|
</script>
|
|
|
|
<form method="POST" {action} use:enhance class="space-y-4">
|
|
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
<div>
|
|
<label for="name" class="mb-1 block text-sm font-medium text-card-foreground">
|
|
Name <span class="text-destructive">*</span>
|
|
</label>
|
|
<input
|
|
id="name"
|
|
name="name"
|
|
type="text"
|
|
bind:value={$form.name}
|
|
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
|
placeholder="My Application"
|
|
/>
|
|
{#if $errors.name}
|
|
<p class="mt-1 text-sm text-destructive">{$errors.name[0]}</p>
|
|
{/if}
|
|
</div>
|
|
|
|
<div>
|
|
<label for="url" class="mb-1 block text-sm font-medium text-card-foreground">
|
|
URL <span class="text-destructive">*</span>
|
|
</label>
|
|
<input
|
|
id="url"
|
|
name="url"
|
|
type="url"
|
|
bind:value={$form.url}
|
|
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
|
placeholder="https://my-app.local:8080"
|
|
/>
|
|
{#if $errors.url}
|
|
<p class="mt-1 text-sm text-destructive">{$errors.url[0]}</p>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label for="description" class="mb-1 block text-sm font-medium text-card-foreground">
|
|
Description
|
|
</label>
|
|
<input
|
|
id="description"
|
|
name="description"
|
|
type="text"
|
|
bind:value={$form.description}
|
|
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
|
placeholder="Brief description of this app"
|
|
/>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
<div>
|
|
<label for="category" class="mb-1 block text-sm font-medium text-card-foreground">
|
|
Category
|
|
</label>
|
|
<input
|
|
id="category"
|
|
name="category"
|
|
type="text"
|
|
bind:value={$form.category}
|
|
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
|
placeholder="e.g. Media, Monitoring, Storage"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label for="tags" class="mb-1 block text-sm font-medium text-card-foreground">
|
|
Tags
|
|
</label>
|
|
<input
|
|
id="tags"
|
|
name="tags"
|
|
type="text"
|
|
bind:value={$form.tags}
|
|
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
|
placeholder="Comma-separated tags"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<AppIconPicker
|
|
iconType={$form.iconType ?? 'lucide'}
|
|
iconValue={$form.icon ?? ''}
|
|
onchange={(type, value) => {
|
|
$form.iconType = type;
|
|
$form.icon = value;
|
|
}}
|
|
/>
|
|
<input type="hidden" name="icon" value={$form.icon ?? ''} />
|
|
<input type="hidden" name="iconType" value={$form.iconType ?? 'lucide'} />
|
|
|
|
<button
|
|
type="button"
|
|
onclick={() => (showAdvanced = !showAdvanced)}
|
|
class="text-sm text-muted-foreground hover:text-foreground"
|
|
>
|
|
{showAdvanced ? 'Hide' : 'Show'} Healthcheck Settings
|
|
</button>
|
|
|
|
{#if showAdvanced}
|
|
<div class="space-y-4 rounded-md border border-border p-4">
|
|
<div class="flex items-center gap-2">
|
|
<input
|
|
id="healthcheckEnabled"
|
|
name="healthcheckEnabled"
|
|
type="checkbox"
|
|
bind:checked={$form.healthcheckEnabled}
|
|
class="rounded border-input"
|
|
/>
|
|
<label for="healthcheckEnabled" class="text-sm text-card-foreground">
|
|
Enable Healthcheck
|
|
</label>
|
|
</div>
|
|
|
|
{#if $form.healthcheckEnabled}
|
|
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
|
<div>
|
|
<label
|
|
for="healthcheckMethod"
|
|
class="mb-1 block text-sm font-medium text-card-foreground"
|
|
>
|
|
Method
|
|
</label>
|
|
<select
|
|
id="healthcheckMethod"
|
|
name="healthcheckMethod"
|
|
bind:value={$form.healthcheckMethod}
|
|
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
|
>
|
|
<option value="GET">GET</option>
|
|
<option value="HEAD">HEAD</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label
|
|
for="healthcheckExpectedStatus"
|
|
class="mb-1 block text-sm font-medium text-card-foreground"
|
|
>
|
|
Expected Status
|
|
</label>
|
|
<input
|
|
id="healthcheckExpectedStatus"
|
|
name="healthcheckExpectedStatus"
|
|
type="number"
|
|
bind:value={$form.healthcheckExpectedStatus}
|
|
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
|
min="100"
|
|
max="599"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label
|
|
for="healthcheckTimeout"
|
|
class="mb-1 block text-sm font-medium text-card-foreground"
|
|
>
|
|
Timeout (ms)
|
|
</label>
|
|
<input
|
|
id="healthcheckTimeout"
|
|
name="healthcheckTimeout"
|
|
type="number"
|
|
bind:value={$form.healthcheckTimeout}
|
|
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
|
min="1000"
|
|
max="30000"
|
|
step="1000"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label
|
|
for="healthcheckInterval"
|
|
class="mb-1 block text-sm font-medium text-card-foreground"
|
|
>
|
|
Interval (seconds)
|
|
</label>
|
|
<input
|
|
id="healthcheckInterval"
|
|
name="healthcheckInterval"
|
|
type="number"
|
|
bind:value={$form.healthcheckInterval}
|
|
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
|
min="30"
|
|
max="86400"
|
|
/>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
|
|
<div class="flex justify-end">
|
|
<button
|
|
type="submit"
|
|
disabled={$submitting}
|
|
class="rounded-md bg-primary px-6 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
|
>
|
|
{#if $submitting}
|
|
Saving...
|
|
{:else}
|
|
Save App
|
|
{/if}
|
|
</button>
|
|
</div>
|
|
</form>
|