feat(phase3): import/export, sparklines, user theme overrides

- JSON import/export with conflict resolution (skip/overwrite) + admin UI
- Ping history sparklines on AppWidget and AppCard (24h, 288 points)
- Hourly cleanup job for old AppStatus records
- User theme preferences (hue, saturation, mode, background, locale)
- Settings page with ThemeCustomizer (sliders, toggles, live preview)
- Prisma migration for user preference fields
- i18n translations for all new strings (EN/RU)
This commit is contained in:
2026-03-25 00:51:01 +03:00
parent d155b3ce4a
commit c6a7de895d
30 changed files with 1633 additions and 44 deletions
@@ -1,5 +1,8 @@
<script lang="ts">
import { t } from 'svelte-i18n';
import { onMount } from 'svelte';
import AppHealthBadge from '$lib/components/app/AppHealthBadge.svelte';
import SparklineChart from '$lib/components/app/SparklineChart.svelte';
interface AppData {
id: string;
@@ -11,12 +14,21 @@
statuses: Array<{ status: string; responseTime: number | null }>;
}
interface StatusPoint {
status: string;
checkedAt: string;
}
interface Props {
app: AppData;
}
let { app }: Props = $props();
let historyData: StatusPoint[] = $state([]);
let uptimePercent: number | null = $state(null);
let historyLoading = $state(true);
const latestStatus = $derived(app.statuses[0]?.status ?? 'unknown');
const iconSrc = $derived.by(() => {
@@ -33,6 +45,23 @@
return null;
}
});
onMount(async () => {
try {
const res = await fetch(`/api/apps/${app.id}/history`);
if (res.ok) {
const json = await res.json();
if (json.success && json.data) {
historyData = json.data.history ?? [];
uptimePercent = json.data.uptimePercent ?? null;
}
}
} catch {
// Silently fail — sparkline is non-critical
} finally {
historyLoading = false;
}
});
</script>
<a
@@ -65,4 +94,16 @@
<!-- Status -->
<AppHealthBadge status={latestStatus} />
<!-- Sparkline -->
{#if historyLoading}
<div class="h-5 w-20 animate-pulse rounded bg-muted"></div>
{:else if historyData.length > 0}
<div class="flex items-center gap-1">
<SparklineChart data={historyData} />
{#if uptimePercent !== null}
<span class="text-[10px] text-muted-foreground">{uptimePercent}%</span>
{/if}
</div>
{/if}
</a>