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:
@@ -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
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -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>
|
||||
@@ -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 },
|
||||
|
||||
@@ -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 />
|
||||
Reference in New Issue
Block a user