feat: Phases 4-7 — Full Feature Expansion (26 features)

Phase 4 — New Widget Types:
- Clock/Weather, System Stats, RSS/Feed, Calendar, Markdown,
  Metric/Counter, Link Group, Camera/Stream widgets
- Backend services with caching for each data source
- Full creation form with dynamic config fields per type

Phase 5 — Visual & Styling Enhancements:
- Glassmorphism card style (solid/glass/outline)
- Board-level themes with per-board hue/saturation
- Animated SVG status rings replacing static dots
- Card size options (compact/medium/large)
- Custom CSS injection (admin + per-board, sanitized)
- Wallpaper backgrounds with blur/overlay/parallax

Phase 6 — Functional Features:
- Favorites bar with drag-and-drop reordering
- Recent apps tracking with privacy toggle
- Uptime dashboard page (/status, guest-accessible)
- Notifications system (Discord/Slack/Telegram/HTTP webhooks)
- App tags with filtering in board view
- Multi-URL app cards with expandable sub-links
- Personal API tokens with scoped permissions
- Audit log with retention and admin viewer

Phase 7 — Quality of Life:
- Onboarding wizard (5-step first-launch setup)
- App URL health preview with favicon/title detection
- Board templates (4 built-in + custom import/export)
- Keyboard shortcut overlay (j/k nav, 1-9 boards, ? help)

212 files changed, 15641 insertions, 980 deletions.
Build, lint, type check, and 222 tests all pass.
This commit is contained in:
2026-03-25 14:18:10 +03:00
parent 8d7847889e
commit 1c0a7cb850
212 changed files with 15642 additions and 981 deletions
+3 -1
View File
@@ -9,6 +9,8 @@
const navItems = $derived([
{ href: '/admin/users', labelKey: 'admin.users' },
{ href: '/admin/groups', labelKey: 'admin.groups' },
{ href: '/admin/tags', label: 'Tags' },
{ href: '/admin/audit-log', label: 'Audit Log' },
{ href: '/admin/settings', labelKey: 'admin.settings' }
]);
@@ -30,7 +32,7 @@
? 'bg-primary text-primary-foreground'
: 'text-muted-foreground hover:bg-accent hover:text-foreground'}"
>
{$t(item.labelKey)}
{item.labelKey ? $t(item.labelKey) : item.label}
</a>
{/each}
</div>
@@ -0,0 +1,46 @@
import type { PageServerLoad } from './$types.js';
import { requireAdmin } from '$lib/server/middleware/authorize.js';
import * as auditLogService from '$lib/server/services/auditLogService.js';
export const load: PageServerLoad = async (event) => {
requireAdmin(event);
const url = event.url;
const action = url.searchParams.get('action') || undefined;
const entityType = url.searchParams.get('entityType') || undefined;
const dateFrom = url.searchParams.get('dateFrom') || undefined;
const dateTo = url.searchParams.get('dateTo') || undefined;
const page = Math.max(1, parseInt(url.searchParams.get('page') ?? '1', 10) || 1);
const limit = 25;
try {
const result = await auditLogService.getAuditLogs({
action,
entityType,
startDate: dateFrom,
endDate: dateTo,
limit,
offset: (page - 1) * limit
});
return {
logs: result.logs,
total: result.total,
filters: {
action: action ?? '',
entityType: entityType ?? '',
dateFrom: dateFrom ?? '',
dateTo: dateTo ?? ''
},
page,
hasMore: result.logs.length === limit
};
} catch {
return {
logs: [],
filters: { action: '', entityType: '', dateFrom: '', dateTo: '' },
page: 1,
hasMore: false
};
}
};
+27
View File
@@ -0,0 +1,27 @@
<script lang="ts">
import { t } from 'svelte-i18n';
import type { PageData } from './$types.js';
import AuditLogTable from '$lib/components/admin/AuditLogTable.svelte';
let { data }: { data: PageData } = $props();
</script>
<svelte:head>
<title>Audit Log — {$t('admin.panel')}</title>
</svelte:head>
<div>
<div class="mb-6">
<h1 class="text-2xl font-bold text-card-foreground">Audit Log</h1>
<p class="mt-1 text-sm text-muted-foreground">
View all administrative actions performed on the system
</p>
</div>
<AuditLogTable
logs={data.logs}
filters={data.filters}
page={data.page}
hasMore={data.hasMore}
/>
</div>
+6 -3
View File
@@ -59,13 +59,16 @@ export const actions: Actions = {
const input = form.data;
if (input.authMode !== undefined) data.authMode = input.authMode;
if (input.registrationEnabled !== undefined) data.registrationEnabled = input.registrationEnabled;
if (input.registrationEnabled !== undefined)
data.registrationEnabled = input.registrationEnabled;
if (input.oauthClientId !== undefined) data.oauthClientId = input.oauthClientId;
if (input.oauthClientSecret !== undefined) data.oauthClientSecret = input.oauthClientSecret;
if (input.oauthDiscoveryUrl !== undefined) data.oauthDiscoveryUrl = input.oauthDiscoveryUrl;
if (input.defaultTheme !== undefined) data.defaultTheme = input.defaultTheme;
if (input.defaultPrimaryColor !== undefined) data.defaultPrimaryColor = input.defaultPrimaryColor;
if (input.healthcheckDefaults !== undefined) data.healthcheckDefaults = input.healthcheckDefaults;
if (input.defaultPrimaryColor !== undefined)
data.defaultPrimaryColor = input.defaultPrimaryColor;
if (input.healthcheckDefaults !== undefined)
data.healthcheckDefaults = input.healthcheckDefaults;
await prisma.systemSettings.upsert({
where: { id: DEFAULTS.SYSTEM_SETTINGS_ID },
+10
View File
@@ -0,0 +1,10 @@
<script lang="ts">
import { t } from 'svelte-i18n';
import TagManager from '$lib/components/admin/TagManager.svelte';
</script>
<svelte:head>
<title>Tag Management — {$t('admin.panel')}</title>
</svelte:head>
<TagManager />