From effa57fd61968082ca8e5a200d04ec977d067616 Mon Sep 17 00:00:00 2001 From: Till JS Date: Thu, 19 Mar 2026 21:59:47 +0100 Subject: [PATCH] feat(manacore): add Mukke, Presi, Context dashboard widgets All apps now have dashboard widgets: - Mukke: music library stats, recent/favorite songs, formatDuration() - Presi: presentation decks, recent decks, deck counts - Context: spaces, recent documents, token balance Added 3 widget types to registry (16 total), 3 API services, i18n translations (DE + EN), and 17 new tests (120 total). Co-Authored-By: Claude Opus 4.6 (1M context) --- .../web/src/lib/api/services/context.test.ts | 127 ++++++++++++++++++ .../apps/web/src/lib/api/services/context.ts | 113 ++++++++++++++++ .../apps/web/src/lib/api/services/index.ts | 8 ++ .../web/src/lib/api/services/mukke.test.ts | 92 +++++++++++++ .../apps/web/src/lib/api/services/mukke.ts | 98 ++++++++++++++ .../web/src/lib/api/services/presi.test.ts | 100 ++++++++++++++ .../apps/web/src/lib/api/services/presi.ts | 101 ++++++++++++++ .../apps/web/src/lib/i18n/locales/de.json | 25 ++++ .../apps/web/src/lib/i18n/locales/en.json | 25 ++++ .../web/src/lib/stores/dashboard.svelte.ts | 3 + .../apps/web/src/lib/types/dashboard.test.ts | 10 +- .../apps/web/src/lib/types/dashboard.ts | 35 ++++- 12 files changed, 734 insertions(+), 3 deletions(-) create mode 100644 apps/manacore/apps/web/src/lib/api/services/context.test.ts create mode 100644 apps/manacore/apps/web/src/lib/api/services/context.ts create mode 100644 apps/manacore/apps/web/src/lib/api/services/mukke.test.ts create mode 100644 apps/manacore/apps/web/src/lib/api/services/mukke.ts create mode 100644 apps/manacore/apps/web/src/lib/api/services/presi.test.ts create mode 100644 apps/manacore/apps/web/src/lib/api/services/presi.ts diff --git a/apps/manacore/apps/web/src/lib/api/services/context.test.ts b/apps/manacore/apps/web/src/lib/api/services/context.test.ts new file mode 100644 index 000000000..f3eb8b7c3 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/api/services/context.test.ts @@ -0,0 +1,127 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; + +// Mock $app/environment +vi.mock('$app/environment', () => ({ + browser: true, +})); + +// Mock auth store +vi.mock('$lib/stores/auth.svelte', () => ({ + authStore: { + getAccessToken: vi.fn().mockResolvedValue('test-token'), + }, +})); + +import { contextService, type ContextSpace, type ContextDocument } from './context'; + +describe('contextService', () => { + beforeEach(() => { + vi.restoreAllMocks(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('getSpaces', () => { + it('should fetch spaces', async () => { + const spaces: ContextSpace[] = [ + { + id: 's-1', + userId: 'u-1', + name: 'Research', + pinned: true, + prefix: 'R', + createdAt: '2026-01-01', + updatedAt: '2026-01-01', + }, + ]; + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(spaces), + }); + + const result = await contextService.getSpaces(); + + expect(result.data).toEqual(spaces); + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining('/spaces'), + expect.any(Object) + ); + }); + }); + + describe('getRecentDocuments', () => { + it('should fetch recent documents with limit', async () => { + const docs: ContextDocument[] = [ + { + id: 'd-1', + userId: 'u-1', + spaceId: 's-1', + title: 'Notes', + type: 'text', + shortId: 'RT1', + pinned: false, + createdAt: '2026-01-01', + updatedAt: '2026-03-01', + }, + ]; + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(docs), + }); + + const result = await contextService.getRecentDocuments(3); + + expect(result.data).toEqual(docs); + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining('/documents/recent?limit=3'), + expect.any(Object) + ); + }); + }); + + describe('getTokenBalance', () => { + it('should fetch token balance', async () => { + const balance = { tokenBalance: 5000, monthlyFreeTokens: 10000 }; + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(balance), + }); + + const result = await contextService.getTokenBalance(); + + expect(result.data).toEqual(balance); + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining('/tokens/balance'), + expect.any(Object) + ); + }); + }); + + describe('getCounts', () => { + it('should return space count', async () => { + const spaces = [ + { id: 's-1', name: 'A' }, + { id: 's-2', name: 'B' }, + ]; + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(spaces), + }); + + const result = await contextService.getCounts(); + + expect(result.data).toEqual({ spaces: 2, documents: 0 }); + }); + + it('should return error on failure', async () => { + global.fetch = vi.fn().mockRejectedValue(new Error('Failed to fetch')); + + const result = await contextService.getCounts(); + + expect(result.data).toBeNull(); + expect(result.error).toBeTruthy(); + }); + }); +}); diff --git a/apps/manacore/apps/web/src/lib/api/services/context.ts b/apps/manacore/apps/web/src/lib/api/services/context.ts new file mode 100644 index 000000000..e0ce89df9 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/api/services/context.ts @@ -0,0 +1,113 @@ +/** + * Context API Service + * + * Fetches documents and spaces from the Context backend for dashboard widgets. + */ + +import { browser } from '$app/environment'; +import { createApiClient, type ApiResult } from '../base-client'; + +// Get Context API URL dynamically at runtime +function getContextApiUrl(): string { + if (browser && typeof window !== 'undefined') { + const injectedUrl = (window as unknown as { __PUBLIC_CONTEXT_API_URL__?: string }) + .__PUBLIC_CONTEXT_API_URL__; + if (injectedUrl) { + return `${injectedUrl}/api/v1`; + } + } + return 'http://localhost:3020/api/v1'; +} + +// Lazy-initialized client +let _client: ReturnType | null = null; + +function getClient() { + if (!_client) { + _client = createApiClient(getContextApiUrl()); + } + return _client; +} + +/** + * Space entity from Context backend + */ +export interface ContextSpace { + id: string; + userId: string; + name: string; + description?: string; + pinned: boolean; + prefix: string; + createdAt: string; + updatedAt: string; +} + +/** + * Document entity from Context backend + */ +export interface ContextDocument { + id: string; + userId: string; + spaceId: string; + title: string; + content?: string; + type: 'text' | 'context' | 'prompt'; + shortId: string; + pinned: boolean; + createdAt: string; + updatedAt: string; +} + +/** + * Token balance from Context backend + */ +export interface TokenBalance { + tokenBalance: number; + monthlyFreeTokens: number; +} + +/** + * Context service for dashboard widgets + */ +export const contextService = { + /** + * Get user's spaces + */ + async getSpaces(): Promise> { + return getClient().get('/spaces'); + }, + + /** + * Get recent documents + */ + async getRecentDocuments(limit = 5): Promise> { + return getClient().get(`/documents/recent?limit=${limit}`); + }, + + /** + * Get token balance + */ + async getTokenBalance(): Promise> { + return getClient().get('/tokens/balance'); + }, + + /** + * Get document and space counts + */ + async getCounts(): Promise> { + const spacesResult = await this.getSpaces(); + + if (spacesResult.error || !spacesResult.data) { + return { data: null, error: spacesResult.error }; + } + + return { + data: { + spaces: spacesResult.data.length, + documents: 0, // Would need a separate count endpoint + }, + error: null, + }; + }, +}; 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 306650e11..b55b8f1d8 100644 --- a/apps/manacore/apps/web/src/lib/api/services/index.ts +++ b/apps/manacore/apps/web/src/lib/api/services/index.ts @@ -13,3 +13,11 @@ export { pictureService, type GeneratedImage, type GenerationStats } from './pic 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'; +export { mukkeService, type Song, type MukkeStats } from './mukke'; +export { presiService, type PresiDeck, type PresiStats } from './presi'; +export { + contextService, + type ContextSpace, + type ContextDocument, + type TokenBalance, +} from './context'; diff --git a/apps/manacore/apps/web/src/lib/api/services/mukke.test.ts b/apps/manacore/apps/web/src/lib/api/services/mukke.test.ts new file mode 100644 index 000000000..42f54d5b6 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/api/services/mukke.test.ts @@ -0,0 +1,92 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; + +// Mock $app/environment +vi.mock('$app/environment', () => ({ + browser: true, +})); + +// Mock auth store +vi.mock('$lib/stores/auth.svelte', () => ({ + authStore: { + getAccessToken: vi.fn().mockResolvedValue('test-token'), + }, +})); + +import { mukkeService } from './mukke'; + +describe('mukkeService', () => { + beforeEach(() => { + vi.restoreAllMocks(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('formatDuration', () => { + it('should format 0 seconds', () => { + expect(mukkeService.formatDuration(0)).toBe('0:00'); + }); + + it('should format seconds only', () => { + expect(mukkeService.formatDuration(45)).toBe('0:45'); + }); + + it('should format minutes and seconds', () => { + expect(mukkeService.formatDuration(185)).toBe('3:05'); + }); + + it('should format hours', () => { + expect(mukkeService.formatDuration(3661)).toBe('1:01:01'); + }); + + it('should pad seconds with zero', () => { + expect(mukkeService.formatDuration(60)).toBe('1:00'); + }); + + it('should handle negative values', () => { + expect(mukkeService.formatDuration(-10)).toBe('0:00'); + }); + }); + + describe('getStats', () => { + it('should fetch library stats', async () => { + const mockStats = { + totalSongs: 42, + totalPlaylists: 5, + totalProjects: 3, + favoriteCount: 10, + totalPlayTime: 7200, + }; + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(mockStats), + }); + + const result = await mukkeService.getStats(); + + expect(result.data).toEqual(mockStats); + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining('/library/stats'), + expect.any(Object) + ); + }); + }); + + describe('getRecentSongs', () => { + it('should fetch recent songs with default limit', async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve([{ id: 's-1', title: 'Song 1' }]), + }); + + const result = await mukkeService.getRecentSongs(); + + expect(result.data).toHaveLength(1); + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining('/songs?limit=5'), + expect.any(Object) + ); + }); + }); +}); diff --git a/apps/manacore/apps/web/src/lib/api/services/mukke.ts b/apps/manacore/apps/web/src/lib/api/services/mukke.ts new file mode 100644 index 000000000..4150ea91b --- /dev/null +++ b/apps/manacore/apps/web/src/lib/api/services/mukke.ts @@ -0,0 +1,98 @@ +/** + * Mukke API Service + * + * Fetches music library stats from the Mukke backend for dashboard widgets. + */ + +import { browser } from '$app/environment'; +import { createApiClient, type ApiResult } from '../base-client'; + +// Get Mukke API URL dynamically at runtime +function getMukkeApiUrl(): string { + if (browser && typeof window !== 'undefined') { + const injectedUrl = (window as unknown as { __PUBLIC_MUKKE_API_URL__?: string }) + .__PUBLIC_MUKKE_API_URL__; + if (injectedUrl) { + return `${injectedUrl}`; + } + } + return 'http://localhost:3010'; +} + +// Lazy-initialized client +let _client: ReturnType | null = null; + +function getClient() { + if (!_client) { + _client = createApiClient(getMukkeApiUrl()); + } + return _client; +} + +/** + * Song entity from Mukke backend + */ +export interface Song { + id: string; + userId: string; + title: string; + artist?: string; + album?: string; + genre?: string; + duration?: number; + favorite: boolean; + playCount: number; + lastPlayedAt?: string; + addedAt: string; +} + +/** + * Music library statistics + */ +export interface MukkeStats { + totalSongs: number; + totalPlaylists: number; + totalProjects: number; + favoriteCount: number; + totalPlayTime: number; // In seconds +} + +/** + * Mukke service for dashboard widgets + */ +export const mukkeService = { + /** + * Get library statistics + */ + async getStats(): Promise> { + return getClient().get('/library/stats'); + }, + + /** + * Get recent songs + */ + async getRecentSongs(limit = 5): Promise> { + return getClient().get(`/songs?limit=${limit}`); + }, + + /** + * Get favorite songs + */ + async getFavoriteSongs(limit = 5): Promise> { + return getClient().get(`/songs?favorite=true&limit=${limit}`); + }, + + /** + * Format duration for display (seconds → MM:SS or HH:MM:SS) + */ + formatDuration(seconds: number): string { + if (seconds <= 0) return '0:00'; + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds % 3600) / 60); + const s = Math.floor(seconds % 60); + if (h > 0) { + return `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`; + } + return `${m}:${String(s).padStart(2, '0')}`; + }, +}; diff --git a/apps/manacore/apps/web/src/lib/api/services/presi.test.ts b/apps/manacore/apps/web/src/lib/api/services/presi.test.ts new file mode 100644 index 000000000..55a79b557 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/api/services/presi.test.ts @@ -0,0 +1,100 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; + +// Mock $app/environment +vi.mock('$app/environment', () => ({ + browser: true, +})); + +// Mock auth store +vi.mock('$lib/stores/auth.svelte', () => ({ + authStore: { + getAccessToken: vi.fn().mockResolvedValue('test-token'), + }, +})); + +import { presiService, type PresiDeck } from './presi'; + +const mockDeck = (overrides: Partial = {}): PresiDeck => ({ + id: 'd-1', + userId: 'u-1', + title: 'Test Deck', + isPublic: false, + createdAt: '2026-01-01', + updatedAt: '2026-03-01', + ...overrides, +}); + +describe('presiService', () => { + beforeEach(() => { + vi.restoreAllMocks(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('getDecks', () => { + it('should fetch decks', async () => { + const decks = [mockDeck()]; + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(decks), + }); + + const result = await presiService.getDecks(); + + expect(result.data).toEqual(decks); + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining('/decks'), + expect.any(Object) + ); + }); + }); + + describe('getRecentDecks', () => { + it('should sort by updatedAt and limit', async () => { + const decks = [ + mockDeck({ id: 'd-1', updatedAt: '2026-01-01' }), + mockDeck({ id: 'd-2', updatedAt: '2026-03-01' }), + mockDeck({ id: 'd-3', updatedAt: '2026-02-01' }), + ]; + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(decks), + }); + + const result = await presiService.getRecentDecks(2); + + expect(result.data).toHaveLength(2); + expect(result.data![0].id).toBe('d-2'); + expect(result.data![1].id).toBe('d-3'); + }); + }); + + describe('getDeckCount', () => { + it('should count total and public decks', async () => { + const decks = [ + mockDeck({ isPublic: true }), + mockDeck({ id: 'd-2', isPublic: false }), + mockDeck({ id: 'd-3', isPublic: true }), + ]; + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(decks), + }); + + const result = await presiService.getDeckCount(); + + expect(result.data).toEqual({ total: 3, public: 2 }); + }); + + it('should return error on failure', async () => { + global.fetch = vi.fn().mockRejectedValue(new Error('Failed to fetch')); + + const result = await presiService.getDeckCount(); + + expect(result.data).toBeNull(); + expect(result.error).toBeTruthy(); + }); + }); +}); diff --git a/apps/manacore/apps/web/src/lib/api/services/presi.ts b/apps/manacore/apps/web/src/lib/api/services/presi.ts new file mode 100644 index 000000000..6b29c5157 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/api/services/presi.ts @@ -0,0 +1,101 @@ +/** + * Presi API Service + * + * Fetches presentation decks from the Presi backend for dashboard widgets. + */ + +import { browser } from '$app/environment'; +import { createApiClient, type ApiResult } from '../base-client'; + +// Get Presi API URL dynamically at runtime +function getPresiApiUrl(): string { + if (browser && typeof window !== 'undefined') { + const injectedUrl = (window as unknown as { __PUBLIC_PRESI_API_URL__?: string }) + .__PUBLIC_PRESI_API_URL__; + if (injectedUrl) { + return `${injectedUrl}/api`; + } + } + return 'http://localhost:3008/api'; +} + +// Lazy-initialized client +let _client: ReturnType | null = null; + +function getClient() { + if (!_client) { + _client = createApiClient(getPresiApiUrl()); + } + return _client; +} + +/** + * Deck entity from Presi backend + */ +export interface PresiDeck { + id: string; + userId: string; + title: string; + description?: string; + themeId?: string; + isPublic: boolean; + createdAt: string; + updatedAt: string; +} + +/** + * Presi statistics + */ +export interface PresiStats { + totalDecks: number; + publicDecks: number; + recentDecks: PresiDeck[]; +} + +/** + * Presi service for dashboard widgets + */ +export const presiService = { + /** + * Get user's decks + */ + async getDecks(): Promise> { + return getClient().get('/decks'); + }, + + /** + * Get recent decks + */ + async getRecentDecks(limit = 5): Promise> { + const result = await this.getDecks(); + + if (result.error || !result.data) { + return result; + } + + const sorted = result.data + .sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()) + .slice(0, limit); + + return { data: sorted, error: null }; + }, + + /** + * Get deck count + */ + async getDeckCount(): Promise> { + const result = await this.getDecks(); + + if (result.error || !result.data) { + return { data: null, error: result.error }; + } + + return { + data: { + total: result.data.length, + public: result.data.filter((d) => d.isPublic).length, + }, + error: null, + }; + }, +}; 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 aa45b3fae..26c0ee109 100644 --- a/apps/manacore/apps/web/src/lib/i18n/locales/de.json +++ b/apps/manacore/apps/web/src/lib/i18n/locales/de.json @@ -101,6 +101,31 @@ "recent": "Kürzlich", "empty": "Keine Dateien", "open": "Storage öffnen" + }, + "mukke": { + "title": "Musik", + "description": "Deine Musikbibliothek", + "songs": "Songs", + "playlists": "Playlists", + "favorites": "Favoriten", + "empty": "Keine Songs", + "open": "Mukke öffnen" + }, + "presi": { + "title": "Präsentationen", + "description": "Deine Slide Decks", + "decks": "Decks", + "empty": "Keine Präsentationen", + "create": "Deck erstellen", + "open": "Presi öffnen" + }, + "context": { + "title": "Context", + "description": "Deine Dokumente & Spaces", + "spaces": "Spaces", + "documents": "Dokumente", + "empty": "Keine Dokumente", + "open": "Context öffnen" } } }, 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 3a7d36e99..83528e862 100644 --- a/apps/manacore/apps/web/src/lib/i18n/locales/en.json +++ b/apps/manacore/apps/web/src/lib/i18n/locales/en.json @@ -101,6 +101,31 @@ "recent": "Recent", "empty": "No files", "open": "Open Storage" + }, + "mukke": { + "title": "Music", + "description": "Your music library", + "songs": "Songs", + "playlists": "Playlists", + "favorites": "Favorites", + "empty": "No songs", + "open": "Open Mukke" + }, + "presi": { + "title": "Presentations", + "description": "Your slide decks", + "decks": "Decks", + "empty": "No presentations", + "create": "Create deck", + "open": "Open Presi" + }, + "context": { + "title": "Context", + "description": "Your documents & spaces", + "spaces": "Spaces", + "documents": "Documents", + "empty": "No documents", + "open": "Open Context" } } }, diff --git a/apps/manacore/apps/web/src/lib/stores/dashboard.svelte.ts b/apps/manacore/apps/web/src/lib/stores/dashboard.svelte.ts index 89e27b6ad..90210ad72 100644 --- a/apps/manacore/apps/web/src/lib/stores/dashboard.svelte.ts +++ b/apps/manacore/apps/web/src/lib/stores/dashboard.svelte.ts @@ -213,6 +213,9 @@ export const dashboardStore = { 'chat-recent', 'contacts-favorites', 'zitare-quote', + 'mukke-library', + 'presi-decks', + 'context-docs', ] as WidgetType[] ).filter((type) => { const meta = getWidgetMeta(type); diff --git a/apps/manacore/apps/web/src/lib/types/dashboard.test.ts b/apps/manacore/apps/web/src/lib/types/dashboard.test.ts index 277070544..d15bc8f57 100644 --- a/apps/manacore/apps/web/src/lib/types/dashboard.test.ts +++ b/apps/manacore/apps/web/src/lib/types/dashboard.test.ts @@ -8,8 +8,8 @@ import { } from './dashboard'; describe('WIDGET_REGISTRY', () => { - it('should contain 13 widget definitions', () => { - expect(WIDGET_REGISTRY).toHaveLength(13); + it('should contain 16 widget definitions', () => { + expect(WIDGET_REGISTRY).toHaveLength(16); }); it('should have unique types for all widgets', () => { @@ -47,6 +47,9 @@ describe('WIDGET_REGISTRY', () => { 'manadeck', 'clock', 'storage', + 'mukke', + 'presi', + 'context', 'mana-core-auth', undefined, ]; @@ -70,6 +73,9 @@ describe('WIDGET_REGISTRY', () => { expect(types).toContain('manadeck-progress'); expect(types).toContain('clock-timers'); expect(types).toContain('storage-usage'); + expect(types).toContain('mukke-library'); + expect(types).toContain('presi-decks'); + expect(types).toContain('context-docs'); }); it('should have i18n-style name keys', () => { diff --git a/apps/manacore/apps/web/src/lib/types/dashboard.ts b/apps/manacore/apps/web/src/lib/types/dashboard.ts index 08097ff36..182ce30a8 100644 --- a/apps/manacore/apps/web/src/lib/types/dashboard.ts +++ b/apps/manacore/apps/web/src/lib/types/dashboard.ts @@ -20,7 +20,10 @@ export type WidgetType = | 'picture-recent' // Picture API: recent generations | 'manadeck-progress' // ManaDeck API: learning progress | 'clock-timers' // Clock: active timers and alarms - | 'storage-usage'; // Storage: file storage stats + | 'storage-usage' // Storage: file storage stats + | 'mukke-library' // Mukke: music library stats + | 'presi-decks' // Presi: recent presentations + | 'context-docs'; // Context: recent documents & spaces /** * Widget size - maps to CSS Grid columns @@ -115,6 +118,9 @@ export interface WidgetMeta { | 'manadeck' | 'clock' | 'storage' + | 'mukke' + | 'presi' + | 'context' | 'mana-core-auth'; } @@ -238,6 +244,33 @@ export const WIDGET_REGISTRY: WidgetMeta[] = [ allowMultiple: false, requiredBackend: 'storage', }, + { + type: 'mukke-library', + nameKey: 'dashboard.widgets.mukke.title', + descriptionKey: 'dashboard.widgets.mukke.description', + icon: '🎵', + defaultSize: 'medium', + allowMultiple: false, + requiredBackend: 'mukke', + }, + { + type: 'presi-decks', + nameKey: 'dashboard.widgets.presi.title', + descriptionKey: 'dashboard.widgets.presi.description', + icon: '📊', + defaultSize: 'medium', + allowMultiple: false, + requiredBackend: 'presi', + }, + { + type: 'context-docs', + nameKey: 'dashboard.widgets.context.title', + descriptionKey: 'dashboard.widgets.context.description', + icon: '📝', + defaultSize: 'medium', + allowMultiple: false, + requiredBackend: 'context', + }, ]; /**