From 7d1b4a40d22bd5e4f5831d3a2775c3c5c201a9b0 Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Fri, 13 Feb 2026 22:34:10 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(dashboard):=20add=20StorageUsa?= =?UTF-8?q?geWidget?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add new dashboard widget showing storage usage stats: - Total storage used and file count - Recent files list with icons - Link to open Storage app Backend changes: - Add GET /api/v1/files/stats endpoint to storage backend - Returns totalFiles, totalSize, favoriteCount, recentFiles Frontend changes: - Add storageService API client - Add StorageUsageWidget component - Add i18n translations (de/en) - Register widget in dashboard types and container Co-Authored-By: Claude Opus 4.5 --- MANACORE-TODOS.md | 10 +- .../apps/web/src/lib/api/services/index.ts | 1 + .../apps/web/src/lib/api/services/storage.ts | 102 ++++++++++++++++ .../dashboard/WidgetContainer.svelte | 2 + .../widgets/StorageUsageWidget.svelte | 111 ++++++++++++++++++ .../apps/web/src/lib/i18n/locales/de.json | 13 ++ .../apps/web/src/lib/i18n/locales/en.json | 13 ++ .../apps/web/src/lib/types/dashboard.ts | 13 +- .../apps/backend/src/file/file.controller.ts | 5 + .../apps/backend/src/file/file.service.ts | 44 +++++++ 10 files changed, 310 insertions(+), 4 deletions(-) create mode 100644 apps/manacore/apps/web/src/lib/api/services/storage.ts create mode 100644 apps/manacore/apps/web/src/lib/components/dashboard/widgets/StorageUsageWidget.svelte diff --git a/MANACORE-TODOS.md b/MANACORE-TODOS.md index c715754e8..f06cb9c3a 100644 --- a/MANACORE-TODOS.md +++ b/MANACORE-TODOS.md @@ -114,9 +114,9 @@ Archivierte Apps (memoro, storyteller) wurden bereits entfernt. ### 3. ✅ Dashboard-Widgets erweitern (GRÖSSTENTEILS ERLEDIGT) -**Status:** 10 von 13 Widgets implementiert +**Status:** 14 von 16 Widgets implementiert (Finance + Mail fehlen) -**Existierende Widgets (13 Typen):** +**Existierende Widgets (14 Typen):** | Widget | App | Status | | ----------------------- | -------------- | ------ | @@ -133,12 +133,16 @@ Archivierte Apps (memoro, storyteller) wurden bereits entfernt. | PictureRecentWidget | picture | ✅ | | ManadeckProgressWidget | manadeck | ✅ | | ClockTimersWidget | clock | ✅ | +| StorageUsageWidget | storage | ✅ | + +**Neue Widgets (2026-02-13):** + +- [x] StorageUsageWidget - Speichernutzung und letzte Dateien **Noch offen (Backend fehlt noch):** - [ ] FinanceBalanceWidget (finance Backend nötig) - [ ] MailInboxWidget (mail Backend nötig) -- [ ] StorageUsageWidget (storage Backend nötig) --- diff --git a/apps/manacore/apps/web/src/lib/api/services/index.ts b/apps/manacore/apps/web/src/lib/api/services/index.ts index 44e830e53..306650e11 100644 --- a/apps/manacore/apps/web/src/lib/api/services/index.ts +++ b/apps/manacore/apps/web/src/lib/api/services/index.ts @@ -12,3 +12,4 @@ export { zitareService, type Favorite, type Quote, type QuoteList } from './zita export { pictureService, type GeneratedImage, type GenerationStats } from './picture'; export { manadeckService, type Deck, type Card, type LearningProgress } from './manadeck'; export { clockService, type Timer, type Alarm, type ClockStats } from './clock'; +export { storageService, type StorageFile, type StorageStats } from './storage'; diff --git a/apps/manacore/apps/web/src/lib/api/services/storage.ts b/apps/manacore/apps/web/src/lib/api/services/storage.ts new file mode 100644 index 000000000..bfd3456c4 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/api/services/storage.ts @@ -0,0 +1,102 @@ +/** + * Storage API Service + * + * Fetches file storage stats from the Storage backend for dashboard widgets. + */ + +import { browser } from '$app/environment'; +import { createApiClient, type ApiResult } from '../base-client'; + +// Get Storage API URL dynamically at runtime +function getStorageApiUrl(): string { + if (browser && typeof window !== 'undefined') { + // Client-side: use injected window variable (set by hooks.server.ts) + const injectedUrl = (window as unknown as { __PUBLIC_STORAGE_API_URL__?: string }) + .__PUBLIC_STORAGE_API_URL__; + if (injectedUrl) { + return `${injectedUrl}/api/v1`; + } + } + // Fallback for local development + return 'http://localhost:3016/api/v1'; +} + +// Lazy-initialized client to ensure we get the correct URL at runtime +let _client: ReturnType | null = null; + +function getClient() { + if (!_client) { + _client = createApiClient(getStorageApiUrl()); + } + return _client; +} + +/** + * File entity from Storage backend + */ +export interface StorageFile { + id: string; + name: string; + originalName: string; + mimeType: string; + size: number; + storagePath: string; + storageKey: string; + parentFolderId?: string | null; + isFavorite: boolean; + currentVersion: number; + createdAt: string; + updatedAt: string; +} + +/** + * Storage stats from backend + */ +export interface StorageStats { + totalFiles: number; + totalSize: number; + favoriteCount: number; + recentFiles: StorageFile[]; +} + +/** + * Storage service for dashboard widgets + */ +export const storageService = { + /** + * Get storage statistics + */ + async getStats(): Promise> { + const result = await getClient().get('/files/stats'); + + if (result.error || !result.data) { + return { data: null, error: result.error }; + } + + return { data: result.data, error: null }; + }, + + /** + * Get recent files + */ + async getRecentFiles(limit: number = 5): Promise> { + const result = await this.getStats(); + + if (result.error || !result.data) { + return { data: null, error: result.error }; + } + + return { data: result.data.recentFiles.slice(0, limit), error: null }; + }, + + /** + * Format file size for display + */ + formatSize(bytes: number): string { + if (bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + }, +}; diff --git a/apps/manacore/apps/web/src/lib/components/dashboard/WidgetContainer.svelte b/apps/manacore/apps/web/src/lib/components/dashboard/WidgetContainer.svelte index ed01f03cb..a72a94606 100644 --- a/apps/manacore/apps/web/src/lib/components/dashboard/WidgetContainer.svelte +++ b/apps/manacore/apps/web/src/lib/components/dashboard/WidgetContainer.svelte @@ -26,6 +26,7 @@ import PictureRecentWidget from './widgets/PictureRecentWidget.svelte'; import ManadeckProgressWidget from './widgets/ManadeckProgressWidget.svelte'; import ClockTimersWidget from './widgets/ClockTimersWidget.svelte'; + import StorageUsageWidget from './widgets/StorageUsageWidget.svelte'; interface Props { widget: WidgetConfig; @@ -67,6 +68,7 @@ 'picture-recent': PictureRecentWidget, 'manadeck-progress': ManadeckProgressWidget, 'clock-timers': ClockTimersWidget, + 'storage-usage': StorageUsageWidget, } as const; const WidgetComponent = $derived(widgetComponents[widget.type]); diff --git a/apps/manacore/apps/web/src/lib/components/dashboard/widgets/StorageUsageWidget.svelte b/apps/manacore/apps/web/src/lib/components/dashboard/widgets/StorageUsageWidget.svelte new file mode 100644 index 000000000..80b5abe86 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/components/dashboard/widgets/StorageUsageWidget.svelte @@ -0,0 +1,111 @@ + + +
+

+ 💾 + {$_('dashboard.widgets.storage.title')} +

+ + {#if state === 'loading'} + + {:else if state === 'error'} + + {:else if data} +
+ +
+
+

{$_('dashboard.widgets.storage.total_size')}

+

{formatSize(data.totalSize)}

+
+
+

{$_('dashboard.widgets.storage.files')}

+

{data.totalFiles}

+
+
+ + + {#if data.recentFiles && data.recentFiles.length > 0} +
+

+ {$_('dashboard.widgets.storage.recent')} +

+
    + {#each data.recentFiles.slice(0, 3) as file} +
  • + {getFileIcon(file.mimeType)} + {file.name} + {formatSize(file.size)} +
  • + {/each} +
+
+ {:else} +

{$_('dashboard.widgets.storage.empty')}

+ {/if} + + + {$_('dashboard.widgets.storage.open')} + +
+ {/if} +
diff --git a/apps/manacore/apps/web/src/lib/i18n/locales/de.json b/apps/manacore/apps/web/src/lib/i18n/locales/de.json index 4bd7d5853..2502be6f5 100644 --- a/apps/manacore/apps/web/src/lib/i18n/locales/de.json +++ b/apps/manacore/apps/web/src/lib/i18n/locales/de.json @@ -92,6 +92,19 @@ "open": "Clock öffnen", "active_timers": "Aktive Timer", "alarms": "Wecker" + }, + "storage": { + "title": "Speicher", + "description": "Dein Cloud-Speicher", + "total_size": "Genutzt", + "files": "Dateien", + "recent": "Kürzlich", + "empty": "Keine Dateien", + "open": "Storage öffnen" + }, + "referral": { + "title": "Empfehlungen", + "description": "Teile und verdiene Credits" } } }, diff --git a/apps/manacore/apps/web/src/lib/i18n/locales/en.json b/apps/manacore/apps/web/src/lib/i18n/locales/en.json index 4f4ce9120..4919f51ad 100644 --- a/apps/manacore/apps/web/src/lib/i18n/locales/en.json +++ b/apps/manacore/apps/web/src/lib/i18n/locales/en.json @@ -92,6 +92,19 @@ "open": "Open Clock", "active_timers": "Active Timers", "alarms": "Alarms" + }, + "storage": { + "title": "Storage", + "description": "Your cloud storage", + "total_size": "Used", + "files": "Files", + "recent": "Recent", + "empty": "No files", + "open": "Open Storage" + }, + "referral": { + "title": "Referrals", + "description": "Share and earn credits" } } }, diff --git a/apps/manacore/apps/web/src/lib/types/dashboard.ts b/apps/manacore/apps/web/src/lib/types/dashboard.ts index c3bd6113b..d9de6e852 100644 --- a/apps/manacore/apps/web/src/lib/types/dashboard.ts +++ b/apps/manacore/apps/web/src/lib/types/dashboard.ts @@ -20,7 +20,8 @@ export type WidgetType = | 'zitare-quote' // Zitare API: daily inspiration quote | 'picture-recent' // Picture API: recent generations | 'manadeck-progress' // ManaDeck API: learning progress - | 'clock-timers'; // Clock: active timers and alarms + | 'clock-timers' // Clock: active timers and alarms + | 'storage-usage'; // Storage: file storage stats /** * Widget size - maps to CSS Grid columns @@ -114,6 +115,7 @@ export interface WidgetMeta { | 'picture' | 'manadeck' | 'clock' + | 'storage' | 'mana-core-auth'; } @@ -237,6 +239,15 @@ export const WIDGET_REGISTRY: WidgetMeta[] = [ allowMultiple: false, requiredBackend: 'clock', }, + { + type: 'storage-usage', + nameKey: 'dashboard.widgets.storage.title', + descriptionKey: 'dashboard.widgets.storage.description', + icon: '💾', + defaultSize: 'medium', + allowMultiple: false, + requiredBackend: 'storage', + }, ]; /** diff --git a/apps/storage/apps/backend/src/file/file.controller.ts b/apps/storage/apps/backend/src/file/file.controller.ts index 64ea611b5..c18580c2c 100644 --- a/apps/storage/apps/backend/src/file/file.controller.ts +++ b/apps/storage/apps/backend/src/file/file.controller.ts @@ -37,6 +37,11 @@ export class FileController { return this.fileService.findAll(user.userId, parentFolderId); } + @Get('stats') + async getStats(@CurrentUser() user: CurrentUserData) { + return this.fileService.getStats(user.userId); + } + @Get(':id') async findOne(@CurrentUser() user: CurrentUserData, @Param('id') id: string) { return this.fileService.findOne(user.userId, id); diff --git a/apps/storage/apps/backend/src/file/file.service.ts b/apps/storage/apps/backend/src/file/file.service.ts index f43a4f91b..fd87fe58c 100644 --- a/apps/storage/apps/backend/src/file/file.service.ts +++ b/apps/storage/apps/backend/src/file/file.service.ts @@ -163,4 +163,48 @@ export class FileService { const file = await this.findOne(userId, id); return this.storageService.getDownloadUrl(file.storageKey); } + + async getStats(userId: string): Promise<{ + totalFiles: number; + totalSize: number; + favoriteCount: number; + recentFiles: File[]; + }> { + const { count, sum } = await import('drizzle-orm'); + + // Get total files and size + const [stats] = await this.db + .select({ + totalFiles: count(files.id), + totalSize: sum(files.size), + }) + .from(files) + .where(and(eq(files.userId, userId), eq(files.isDeleted, false))); + + // Get favorite count + const [favStats] = await this.db + .select({ + count: count(files.id), + }) + .from(files) + .where( + and(eq(files.userId, userId), eq(files.isDeleted, false), eq(files.isFavorite, true)) + ); + + // Get recent files (last 5) + const { desc } = await import('drizzle-orm'); + const recentFiles = await this.db + .select() + .from(files) + .where(and(eq(files.userId, userId), eq(files.isDeleted, false))) + .orderBy(desc(files.updatedAt)) + .limit(5); + + return { + totalFiles: Number(stats.totalFiles) || 0, + totalSize: Number(stats.totalSize) || 0, + favoriteCount: Number(favStats.count) || 0, + recentFiles, + }; + } }