From bef92cb7d4a660fc1666a68578ae5a2fea58d4a5 Mon Sep 17 00:00:00 2001 From: Wayne <5291640+ringoinca@users.noreply.github.com> Date: Thu, 24 Jul 2025 14:43:24 +0300 Subject: [PATCH] Dashboard revamp --- packages/frontend/src/app.css | 214 +++++------ packages/frontend/src/lib/utils.ts | 18 +- .../src/routes/dashboard/+page.server.ts | 15 +- .../src/routes/dashboard/+page.svelte | 340 +++++++----------- 4 files changed, 267 insertions(+), 320 deletions(-) diff --git a/packages/frontend/src/app.css b/packages/frontend/src/app.css index e48f5f0..b37c9b3 100644 --- a/packages/frontend/src/app.css +++ b/packages/frontend/src/app.css @@ -1,121 +1,121 @@ -@import "tailwindcss"; +@import 'tailwindcss'; -@import "tw-animate-css"; +@import 'tw-animate-css'; @custom-variant dark (&:is(.dark *)); :root { - --radius: 0.625rem; - --background: oklch(1 0 0); - --foreground: oklch(0.129 0.042 264.695); - --card: oklch(1 0 0); - --card-foreground: oklch(0.129 0.042 264.695); - --popover: oklch(1 0 0); - --popover-foreground: oklch(0.129 0.042 264.695); - --primary: oklch(0.208 0.042 265.755); - --primary-foreground: oklch(0.984 0.003 247.858); - --secondary: oklch(0.968 0.007 247.896); - --secondary-foreground: oklch(0.208 0.042 265.755); - --muted: oklch(0.968 0.007 247.896); - --muted-foreground: oklch(0.554 0.046 257.417); - --accent: oklch(0.968 0.007 247.896); - --accent-foreground: oklch(0.208 0.042 265.755); - --destructive: oklch(0.577 0.245 27.325); - --border: oklch(0.929 0.013 255.508); - --input: oklch(0.929 0.013 255.508); - --ring: oklch(0.704 0.04 256.788); - --chart-1: oklch(0.646 0.222 41.116); - --chart-2: oklch(0.6 0.118 184.704); - --chart-3: oklch(0.398 0.07 227.392); - --chart-4: oklch(0.828 0.189 84.429); - --chart-5: oklch(0.769 0.188 70.08); - --sidebar: oklch(0.984 0.003 247.858); - --sidebar-foreground: oklch(0.129 0.042 264.695); - --sidebar-primary: oklch(0.208 0.042 265.755); - --sidebar-primary-foreground: oklch(0.984 0.003 247.858); - --sidebar-accent: oklch(0.968 0.007 247.896); - --sidebar-accent-foreground: oklch(0.208 0.042 265.755); - --sidebar-border: oklch(0.929 0.013 255.508); - --sidebar-ring: oklch(0.704 0.04 256.788); + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.129 0.042 264.695); + --card: oklch(1 0 0); + --card-foreground: oklch(0.129 0.042 264.695); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.129 0.042 264.695); + --primary: oklch(0.208 0.042 265.755); + --primary-foreground: oklch(0.984 0.003 247.858); + --secondary: oklch(0.968 0.007 247.896); + --secondary-foreground: oklch(0.208 0.042 265.755); + --muted: oklch(0.968 0.007 247.896); + --muted-foreground: oklch(0.554 0.046 257.417); + --accent: oklch(0.968 0.007 247.896); + --accent-foreground: oklch(0.208 0.042 265.755); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.929 0.013 255.508); + --input: oklch(0.929 0.013 255.508); + --ring: oklch(0.704 0.04 256.788); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.984 0.003 247.858); + --sidebar-foreground: oklch(0.129 0.042 264.695); + --sidebar-primary: oklch(0.208 0.042 265.755); + --sidebar-primary-foreground: oklch(0.984 0.003 247.858); + --sidebar-accent: oklch(0.968 0.007 247.896); + --sidebar-accent-foreground: oklch(0.208 0.042 265.755); + --sidebar-border: oklch(0.929 0.013 255.508); + --sidebar-ring: oklch(0.704 0.04 256.788); } .dark { - --background: oklch(0.129 0.042 264.695); - --foreground: oklch(0.984 0.003 247.858); - --card: oklch(0.208 0.042 265.755); - --card-foreground: oklch(0.984 0.003 247.858); - --popover: oklch(0.208 0.042 265.755); - --popover-foreground: oklch(0.984 0.003 247.858); - --primary: oklch(0.929 0.013 255.508); - --primary-foreground: oklch(0.208 0.042 265.755); - --secondary: oklch(0.279 0.041 260.031); - --secondary-foreground: oklch(0.984 0.003 247.858); - --muted: oklch(0.279 0.041 260.031); - --muted-foreground: oklch(0.704 0.04 256.788); - --accent: oklch(0.279 0.041 260.031); - --accent-foreground: oklch(0.984 0.003 247.858); - --destructive: oklch(0.704 0.191 22.216); - --border: oklch(1 0 0 / 10%); - --input: oklch(1 0 0 / 15%); - --ring: oklch(0.551 0.027 264.364); - --chart-1: oklch(0.488 0.243 264.376); - --chart-2: oklch(0.696 0.17 162.48); - --chart-3: oklch(0.769 0.188 70.08); - --chart-4: oklch(0.627 0.265 303.9); - --chart-5: oklch(0.645 0.246 16.439); - --sidebar: oklch(0.208 0.042 265.755); - --sidebar-foreground: oklch(0.984 0.003 247.858); - --sidebar-primary: oklch(0.488 0.243 264.376); - --sidebar-primary-foreground: oklch(0.984 0.003 247.858); - --sidebar-accent: oklch(0.279 0.041 260.031); - --sidebar-accent-foreground: oklch(0.984 0.003 247.858); - --sidebar-border: oklch(1 0 0 / 10%); - --sidebar-ring: oklch(0.551 0.027 264.364); + --background: oklch(0.129 0.042 264.695); + --foreground: oklch(0.984 0.003 247.858); + --card: oklch(0.208 0.042 265.755); + --card-foreground: oklch(0.984 0.003 247.858); + --popover: oklch(0.208 0.042 265.755); + --popover-foreground: oklch(0.984 0.003 247.858); + --primary: oklch(0.929 0.013 255.508); + --primary-foreground: oklch(0.208 0.042 265.755); + --secondary: oklch(0.279 0.041 260.031); + --secondary-foreground: oklch(0.984 0.003 247.858); + --muted: oklch(0.279 0.041 260.031); + --muted-foreground: oklch(0.704 0.04 256.788); + --accent: oklch(0.279 0.041 260.031); + --accent-foreground: oklch(0.984 0.003 247.858); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.551 0.027 264.364); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.208 0.042 265.755); + --sidebar-foreground: oklch(0.984 0.003 247.858); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.984 0.003 247.858); + --sidebar-accent: oklch(0.279 0.041 260.031); + --sidebar-accent-foreground: oklch(0.984 0.003 247.858); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.551 0.027 264.364); } @theme inline { - --radius-sm: calc(var(--radius) - 4px); - --radius-md: calc(var(--radius) - 2px); - --radius-lg: var(--radius); - --radius-xl: calc(var(--radius) + 4px); - --color-background: var(--background); - --color-foreground: var(--foreground); - --color-card: var(--card); - --color-card-foreground: var(--card-foreground); - --color-popover: var(--popover); - --color-popover-foreground: var(--popover-foreground); - --color-primary: var(--primary); - --color-primary-foreground: var(--primary-foreground); - --color-secondary: var(--secondary); - --color-secondary-foreground: var(--secondary-foreground); - --color-muted: var(--muted); - --color-muted-foreground: var(--muted-foreground); - --color-accent: var(--accent); - --color-accent-foreground: var(--accent-foreground); - --color-destructive: var(--destructive); - --color-border: var(--border); - --color-input: var(--input); - --color-ring: var(--ring); - --color-chart-1: var(--chart-1); - --color-chart-2: var(--chart-2); - --color-chart-3: var(--chart-3); - --color-chart-4: var(--chart-4); - --color-chart-5: var(--chart-5); - --color-sidebar: var(--sidebar); - --color-sidebar-foreground: var(--sidebar-foreground); - --color-sidebar-primary: var(--sidebar-primary); - --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); - --color-sidebar-accent: var(--sidebar-accent); - --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); - --color-sidebar-border: var(--sidebar-border); - --color-sidebar-ring: var(--sidebar-ring); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); } @layer base { - * { - @apply border-border outline-ring/50; - } - body { - @apply bg-background text-foreground; - } -} \ No newline at end of file + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/packages/frontend/src/lib/utils.ts b/packages/frontend/src/lib/utils.ts index 55b3a91..8993a9c 100644 --- a/packages/frontend/src/lib/utils.ts +++ b/packages/frontend/src/lib/utils.ts @@ -5,9 +5,21 @@ export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } +export function formatBytes(bytes: number, decimals = 2) { + if (bytes === 0) return '0 Bytes'; + + const k = 1024; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; +} + // eslint-disable-next-line @typescript-eslint/no-explicit-any -export type WithoutChild = T extends { child?: any } ? Omit : T; +export type WithoutChild = T extends { child?: any; } ? Omit : T; // eslint-disable-next-line @typescript-eslint/no-explicit-any -export type WithoutChildren = T extends { children?: any } ? Omit : T; +export type WithoutChildren = T extends { children?: any; } ? Omit : T; export type WithoutChildrenOrChild = WithoutChildren>; -export type WithElementRef = T & { ref?: U | null }; +export type WithElementRef = T & { ref?: U | null; }; diff --git a/packages/frontend/src/routes/dashboard/+page.server.ts b/packages/frontend/src/routes/dashboard/+page.server.ts index 2c96002..77acb90 100644 --- a/packages/frontend/src/routes/dashboard/+page.server.ts +++ b/packages/frontend/src/routes/dashboard/+page.server.ts @@ -52,10 +52,17 @@ export const load: PageServerLoad = async (event) => { } }; + const [stats, ingestionHistory, ingestionSources, recentSyncs] = await Promise.all([ + fetchStats(), + fetchIngestionHistory(), + fetchIngestionSources(), + fetchRecentSyncs() + ]); + return { - stats: fetchStats(), - ingestionHistory: fetchIngestionHistory(), - ingestionSources: fetchIngestionSources(), - recentSyncs: fetchRecentSyncs() + stats, + ingestionHistory, + ingestionSources, + recentSyncs }; }; diff --git a/packages/frontend/src/routes/dashboard/+page.svelte b/packages/frontend/src/routes/dashboard/+page.svelte index 20abb15..b4af554 100644 --- a/packages/frontend/src/routes/dashboard/+page.svelte +++ b/packages/frontend/src/routes/dashboard/+page.svelte @@ -1,226 +1,154 @@ Dashboard - OpenArchiver - + -
-

Dashboard

+
+
+

Dashboard

+
- -
-

Ingestion Overview

-
- {#await stats} -

Loading stats...

- {:then statsData} - {#if statsData} - - - Total Emails Archived - - -

{statsData.totalEmailsArchived.toLocaleString()}

-
-
- - - Total Storage Used - - -

{formatBytes(statsData.totalStorageUsed)}

-
-
- - - Failed Ingestions (7 days) - - -

0} - > - {statsData.failedIngestionsLast7Days} -

-
-
- {/if} - {/await} + {#if data.stats} +
+ + + Total Emails Archived + + +
{data.stats.totalEmailsArchived}
+
+
+ + + Total Storage Used + + +
{formatBytes(data.stats.totalStorageUsed)}
+
+
+ + + Failed Ingestions (Last 7 Days) + + +
{data.stats.failedIngestionsLast7Days}
+
+
-
+ {/if} - -
- - - Emails Archived (Last 30 Days) - - - {#await ingestionHistory} -

Loading chart...

- {:then historyData} - {#if historyData} -
- - - -
- {/if} - {/await} -
-
- - - Storage Usage by Source - - - {#await ingestionSources} -

Loading chart...

- {:then sourceData} - {#if sourceData} -
- - - -
- {/if} - {/await} -
-
-
+
+ + + Ingestion History + + + {#if data.ingestionHistory && data.ingestionHistory.history.length > 0} + + + new Date(d).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) + } + }} + > + {#snippet tooltip()} + + {/snippet} + + + {:else} +

No ingestion history available.

+ {/if} +
+
- -
-

Ingestion Source Status

- - - {#await ingestionSources} -

Loading sources...

- {:then sourceData} - {#if sourceData} - - - - Name - Provider - Status - Storage Used - - - - {#each sourceData as source} - - {source.name} - {source.provider} - - - {source.status} - - - {formatBytes(source.storageUsed)} - - {/each} - -
- {/if} - {/await} -
-
-
+ + + Recent Syncs + Most recent sync activities. + + + {#if data.recentSyncs && data.recentSyncs.length > 0} + + + + Source + Status + Processed + + + + {#each data.recentSyncs as sync} + + {sync.sourceName} + {sync.status} + {sync.emailsProcessed} + + {/each} + + + {:else} +

No recent syncs.

+ {/if} +
+
+
- -
-

Recent Sync Jobs

- - - {#await recentSyncs} -

Loading syncs...

- {:then syncData} - {#if syncData && syncData.length > 0} - - - - Source - Start Time - Duration - Emails Processed - Status - - - - {#each syncData as sync} - - {sync.sourceName} - {new Date(sync.startTime).toLocaleString()} - {sync.duration}s - {sync.emailsProcessed} - - - {sync.status} - - - - {/each} - -
- {:else} -

No recent sync jobs found.

- {/if} - {/await} -
-
-
+ + + Ingestion Sources + Overview of your ingestion sources. + + + {#if data.ingestionSources && data.ingestionSources.length > 0} + + + + Name + Provider + Status + Storage Used + + + + {#each data.ingestionSources as source} + + {source.name} + {source.provider} + {source.status} + {formatBytes(source.storageUsed)} + + {/each} + + + {:else} +

No ingestion sources found.

+ {/if} +
+