From be20de23db529e2d3b3b7827f18098615f82b255 Mon Sep 17 00:00:00 2001 From: Till JS Date: Thu, 2 Apr 2026 16:10:10 +0200 Subject: [PATCH] =?UTF-8?q?feat(manacore/web):=20uload=20feature=20parity?= =?UTF-8?q?=20=E2=80=94=20tags,=20analytics,=20settings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port missing features from standalone uload app to unified ManaCore module: - Add Tag Management page (/uload/tags) with CRUD, color picker, inline edit - Replace analytics placeholder with full server dashboard (timeline chart, device/country/referrer breakdowns, graceful fallback when server unavailable) - Add Settings page (/uload/settings) with data overview, JSON export, clear data - Fix bugs: missing LocalTag type, unregistered uloadTags table, wrong table name in AppView (linkFolders → uloadFolders) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../apps/web/src/lib/data/database.ts | 54 +++- .../web/src/lib/modules/uload/AppView.svelte | 2 +- .../web/src/lib/modules/uload/collections.ts | 32 +- .../apps/web/src/lib/modules/uload/types.ts | 10 + .../(app)/uload/analytics/[id]/+page.svelte | 292 +++++++++++++----- .../routes/(app)/uload/settings/+page.svelte | 106 +++++++ .../src/routes/(app)/uload/tags/+page.svelte | 164 ++++++++++ 7 files changed, 567 insertions(+), 93 deletions(-) create mode 100644 apps/manacore/apps/web/src/routes/(app)/uload/settings/+page.svelte create mode 100644 apps/manacore/apps/web/src/routes/(app)/uload/tags/+page.svelte diff --git a/apps/manacore/apps/web/src/lib/data/database.ts b/apps/manacore/apps/web/src/lib/data/database.ts index 19d48a8fe..7ba65c73e 100644 --- a/apps/manacore/apps/web/src/lib/data/database.ts +++ b/apps/manacore/apps/web/src/lib/data/database.ts @@ -33,14 +33,17 @@ db.version(1).stores({ // ─── Calendar (appId: 'calendar') ─── calendars: 'id, isDefault, isVisible', events: 'id, calendarId, startDate, endDate, allDay, [calendarId+startDate]', + eventTags: 'id, eventId, tagId, [eventId+tagId]', // ─── Contacts (appId: 'contacts') ─── contacts: 'id, firstName, lastName, email, company, isFavorite, isArchived', + contactTags: 'id, contactId, tagId, [contactId+tagId]', // ─── Chat (appId: 'chat') ─── conversations: 'id, isArchived, isPinned, spaceId, templateId', messages: 'id, conversationId, sender, [conversationId+sender]', chatTemplates: 'id, isDefault', + conversationTags: 'id, conversationId, tagId, [conversationId+tagId]', // ─── Picture (appId: 'picture') ─── images: 'id, isFavorite, isPublic, archivedAt, prompt', @@ -51,10 +54,12 @@ db.version(1).stores({ // ─── Cards (appId: 'cards') ─── cardDecks: 'id, isPublic', cards: 'id, deckId, difficulty, nextReview, order, [deckId+order]', + deckTags: 'id, deckId, tagId, [deckId+tagId]', // ─── Zitare (appId: 'zitare') ─── zitareFavorites: 'id, quoteId', zitareLists: 'id', + zitareListTags: 'id, listId, tagId, [listId+tagId]', // ─── Mukke (appId: 'mukke') ─── songs: 'id, artist, album, genre, favorite, title', @@ -62,6 +67,7 @@ db.version(1).stores({ playlistSongs: 'id, playlistId, songId, sortOrder, [playlistId+sortOrder]', mukkeProjects: 'id, title, songId', markers: 'id, beatId, type, sortOrder', + songTags: 'id, songId, tagId, [songId+tagId]', // ─── Storage (appId: 'storage') ─── files: 'id, parentFolderId, mimeType, isFavorite, isDeleted, name', @@ -71,12 +77,14 @@ db.version(1).stores({ // ─── Presi (appId: 'presi') ─── presiDecks: 'id, isPublic', slides: 'id, deckId, order, [deckId+order]', + presiDeckTags: 'id, deckId, tagId, [deckId+tagId]', // ─── Inventar (appId: 'inventar') ─── invCollections: 'id, order, templateId', invItems: 'id, collectionId, locationId, categoryId, status, name, [collectionId+order]', invLocations: 'id, parentId, path, depth, order', invCategories: 'id, parentId, order', + invItemTags: 'id, itemId, tagId, [itemId+tagId]', // ─── Photos (appId: 'photos') ─── albums: 'id, isAutoGenerated, name', @@ -88,11 +96,13 @@ db.version(1).stores({ skills: 'id, branch, parentId, level', activities: 'id, skillId, timestamp', achievements: 'id, key, unlockedAt', + skillTags: 'id, skillId, tagId, [skillId+tagId]', // ─── CityCorners (appId: 'citycorners') ─── cities: 'id, slug, country, name', ccLocations: 'id, cityId, category, name', ccFavorites: 'id, locationId', + ccLocationTags: 'id, locationId, tagId, [locationId+tagId]', // ─── Times (appId: 'times') ─── timeClients: 'id, order, isArchived, shortCode', @@ -104,29 +114,35 @@ db.version(1).stores({ timeAlarms: 'id, enabled, time', timeCountdownTimers: 'id, status', timeWorldClocks: 'id, sortOrder, timezone', + entryTags: 'id, entryId, tagId, [entryId+tagId]', // ─── Context (appId: 'context') ─── contextSpaces: 'id, pinned, prefix', documents: 'id, spaceId, type, pinned, title, [spaceId+type]', + documentTags: 'id, documentId, tagId, [documentId+tagId]', // ─── Questions (appId: 'questions') ─── qCollections: 'id, sortOrder, isDefault', questions: 'id, collectionId, status, priority, [collectionId+status]', answers: 'id, questionId, isAccepted', + questionTags: 'id, questionId, tagId, [questionId+tagId]', // ─── NutriPhi (appId: 'nutriphi') ─── meals: 'id, date, mealType, [date+mealType]', goals: 'id', nutriFavorites: 'id, mealType, usageCount', + mealTags: 'id, mealId, tagId, [mealId+tagId]', // ─── Planta (appId: 'planta') ─── plants: 'id, isActive, healthStatus', plantPhotos: 'id, plantId, isPrimary, [plantId+isPrimary]', wateringSchedules: 'id, plantId, nextWateringAt', wateringLogs: 'id, plantId, wateredAt', + plantTags: 'id, plantId, tagId, [plantId+tagId]', // ─── uLoad (appId: 'uload') ─── links: 'id, shortCode, isActive, folderId, order, clickCount, [folderId+order], [isActive+order]', + uloadTags: 'id, slug, name', uloadFolders: 'id, order', linkTags: 'id, linkId, tagId, [linkId+tagId]', @@ -137,6 +153,7 @@ db.version(1).stores({ // ─── Moodlit (appId: 'moodlit') ─── moods: 'id, name, animation, isDefault', sequences: 'id, name', + moodTags: 'id, moodId, tagId, [moodId+tagId]', // ─── Memoro (appId: 'memoro') ─── memos: 'id, processingStatus, isArchived, isPinned, language, [isArchived+createdAt]', @@ -152,6 +169,7 @@ db.version(1).stores({ steps: 'id, guideId, sectionId, order, [guideId+order]', guideCollections: 'id', runs: 'id, guideId, startedAt, completedAt', + guideTags: 'id, guideId, tagId, [guideId+tagId]', // ─── Playground (appId: 'playground') ─── // No persistent data — stateless LLM playground @@ -171,19 +189,19 @@ db.version(1).stores({ export const SYNC_APP_MAP: Record = { manacore: ['userSettings', 'dashboardConfigs'], todo: ['tasks', 'todoProjects', 'taskLabels', 'reminders', 'boardViews'], - calendar: ['calendars', 'events'], - contacts: ['contacts'], - chat: ['conversations', 'messages', 'chatTemplates'], + calendar: ['calendars', 'events', 'eventTags'], + contacts: ['contacts', 'contactTags'], + chat: ['conversations', 'messages', 'chatTemplates', 'conversationTags'], picture: ['images', 'boards', 'boardItems', 'imageTags'], - cards: ['cardDecks', 'cards'], - zitare: ['zitareFavorites', 'zitareLists'], - mukke: ['songs', 'mukkePlaylists', 'playlistSongs', 'mukkeProjects', 'markers'], + cards: ['cardDecks', 'cards', 'deckTags'], + zitare: ['zitareFavorites', 'zitareLists', 'zitareListTags'], + mukke: ['songs', 'mukkePlaylists', 'playlistSongs', 'mukkeProjects', 'markers', 'songTags'], storage: ['files', 'storageFolders', 'fileTags'], - presi: ['presiDecks', 'slides'], - inventar: ['invCollections', 'invItems', 'invLocations', 'invCategories'], + presi: ['presiDecks', 'slides', 'presiDeckTags'], + inventar: ['invCollections', 'invItems', 'invLocations', 'invCategories', 'invItemTags'], photos: ['albums', 'albumItems', 'photoFavorites', 'photoMediaTags'], - skilltree: ['skills', 'activities', 'achievements'], - citycorners: ['cities', 'ccLocations', 'ccFavorites'], + skilltree: ['skills', 'activities', 'achievements', 'skillTags'], + citycorners: ['cities', 'ccLocations', 'ccFavorites', 'ccLocationTags'], times: [ 'timeClients', 'timeProjects', @@ -193,16 +211,17 @@ export const SYNC_APP_MAP: Record = { 'timeAlarms', 'timeCountdownTimers', 'timeWorldClocks', + 'entryTags', ], - context: ['contextSpaces', 'documents'], - questions: ['qCollections', 'questions', 'answers'], - nutriphi: ['meals', 'goals', 'nutriFavorites'], - planta: ['plants', 'plantPhotos', 'wateringSchedules', 'wateringLogs'], - uload: ['links', 'uloadFolders', 'linkTags'], + context: ['contextSpaces', 'documents', 'documentTags'], + questions: ['qCollections', 'questions', 'answers', 'questionTags'], + nutriphi: ['meals', 'goals', 'nutriFavorites', 'mealTags'], + planta: ['plants', 'plantPhotos', 'wateringSchedules', 'wateringLogs', 'plantTags'], + uload: ['links', 'uloadTags', 'uloadFolders', 'linkTags'], calc: ['calculations', 'savedFormulas'], - moodlit: ['moods', 'sequences'], + moodlit: ['moods', 'sequences', 'moodTags'], memoro: ['memos', 'memories', 'memoTags', 'memoroSpaces', 'spaceMembers', 'memoSpaces'], - guides: ['guides', 'sections', 'steps', 'guideCollections', 'runs'], + guides: ['guides', 'sections', 'steps', 'guideCollections', 'runs', 'guideTags'], tags: ['globalTags', 'tagGroups'], links: ['manaLinks'], }; @@ -265,6 +284,7 @@ export const TABLE_TO_SYNC_NAME: Record = { // memoro memoroSpaces: 'spaces', // uload + uloadTags: 'tags', uloadFolders: 'folders', // guides guideCollections: 'collections', diff --git a/apps/manacore/apps/web/src/lib/modules/uload/AppView.svelte b/apps/manacore/apps/web/src/lib/modules/uload/AppView.svelte index 7d2663780..d94c3b6af 100644 --- a/apps/manacore/apps/web/src/lib/modules/uload/AppView.svelte +++ b/apps/manacore/apps/web/src/lib/modules/uload/AppView.svelte @@ -25,7 +25,7 @@ $effect(() => { const sub = liveQuery(async () => { return db - .table('linkFolders') + .table('uloadFolders') .toArray() .then((all) => all.filter((f) => !f.deletedAt)); }).subscribe((val) => { diff --git a/apps/manacore/apps/web/src/lib/modules/uload/collections.ts b/apps/manacore/apps/web/src/lib/modules/uload/collections.ts index 8516f4197..aead39acb 100644 --- a/apps/manacore/apps/web/src/lib/modules/uload/collections.ts +++ b/apps/manacore/apps/web/src/lib/modules/uload/collections.ts @@ -5,11 +5,12 @@ */ import { db } from '$lib/data/database'; -import type { LocalLink, LocalFolder, LocalLinkTag } from './types'; +import type { LocalLink, LocalTag, LocalFolder, LocalLinkTag } from './types'; // ─── Collection Accessors ────────────────────────────────── export const linkTable = db.table('links'); +export const uloadTagTable = db.table('uloadTags'); export const uloadFolderTable = db.table('uloadFolders'); export const linkTagTable = db.table('linkTags'); @@ -79,5 +80,34 @@ export const ULOAD_GUEST_SEED = { order: 1, }, ] satisfies LocalLink[], + uloadTags: [ + { + id: 'tag-social', + name: 'Social Media', + slug: 'social-media', + color: '#8b5cf6', + icon: null, + isPublic: false, + usageCount: 0, + }, + { + id: 'tag-docs', + name: 'Dokumentation', + slug: 'dokumentation', + color: '#3b82f6', + icon: null, + isPublic: false, + usageCount: 0, + }, + { + id: 'tag-marketing', + name: 'Marketing', + slug: 'marketing', + color: '#10b981', + icon: null, + isPublic: false, + usageCount: 0, + }, + ] satisfies LocalTag[], linkTags: [] as LocalLinkTag[], }; diff --git a/apps/manacore/apps/web/src/lib/modules/uload/types.ts b/apps/manacore/apps/web/src/lib/modules/uload/types.ts index c75f1151b..09fcd0029 100644 --- a/apps/manacore/apps/web/src/lib/modules/uload/types.ts +++ b/apps/manacore/apps/web/src/lib/modules/uload/types.ts @@ -21,6 +21,16 @@ export interface LocalLink extends BaseRecord { utmCampaign?: string | null; folderId?: string | null; order: number; + source?: string | null; +} + +export interface LocalTag extends BaseRecord { + name: string; + slug: string; + color?: string | null; + icon?: string | null; + isPublic: boolean; + usageCount: number; } export interface LocalFolder extends BaseRecord { diff --git a/apps/manacore/apps/web/src/routes/(app)/uload/analytics/[id]/+page.svelte b/apps/manacore/apps/web/src/routes/(app)/uload/analytics/[id]/+page.svelte index 14bced654..0c7e8804c 100644 --- a/apps/manacore/apps/web/src/routes/(app)/uload/analytics/[id]/+page.svelte +++ b/apps/manacore/apps/web/src/routes/(app)/uload/analytics/[id]/+page.svelte @@ -1,135 +1,179 @@ Analytics - uLoad - ManaCore -
+
- - + +
-

Analytics

+

Analytics

{#if link} -

- /{link.shortCode} +

+ /{link.shortCode}{link.originalUrl}

{/if}
- {#if !link} + {#if loading}
{#each Array(4) as _} -
+
{/each}
+ {:else if !link} +
+

Link nicht gefunden

+
{:else}
-
-

Clicks

-

{link.clickCount}

+
+

Clicks

+

+ {stats?.totalClicks ?? link.clickCount} +

-
-

Status

+
+

Unique

+

+ {stats?.uniqueVisitors ?? '-'} +

+
+
+

Status

{#if link.isActive} - Aktiv + Aktiv {:else} - Inaktiv + Inaktiv {/if}

-
-

Erstellt

-

+

+

Erstellt

+

{new Date(link.createdAt).toLocaleDateString('de')}

-
-

Short URL

-

- ulo.ad/{link.shortCode} -

-
-
-

Link Details

+
+

Link Details

- Ziel-URL + Ziel-URL {link.originalUrl}
{#if link.title}
- Titel - {link.title} + Titel + {link.title}
{/if} {#if link.utmSource || link.utmMedium || link.utmCampaign} -
-

+

+

UTM-Parameter

{#if link.utmSource} -
- Source: +
+ Source: {link.utmSource}
{/if} {#if link.utmMedium} -
- Medium: +
+ Medium: {link.utmMedium}
{/if} {#if link.utmCampaign} -
- Campaign: +
+ Campaign: {link.utmCampaign}
{/if} @@ -138,52 +182,152 @@ {/if} {#if link.expiresAt}
- Laeuft ab - {new Date(link.expiresAt).toLocaleDateString('de')} + Laeuft ab + {new Date(link.expiresAt).toLocaleDateString('de')}
{/if} {#if link.maxClicks}
- Max Klicks - {link.clickCount} / {link.maxClicks} + Max Klicks + {link.clickCount} / {link.maxClicks}
{/if} {#if link.password}
- Passwortgeschuetzt - Ja + Passwortgeschuetzt + Ja
{/if}
- -
+ +
-

Clicks ueber Zeit

+

Clicks ueber Zeit

{#each [7, 30, 90] as d} {/each}
-
-

- Detaillierte Analytics sind verfuegbar, wenn der uLoad-Server verbunden ist. -

-

- Lokaler Click-Count: {link.clickCount} -

-
+ {#if timeline.length > 0} +
+ {#each timeline as day} +
+
+ +
+ {/each} +
+
+ {timeline[0]?.date} + {timeline[timeline.length - 1]?.date} +
+ {:else if !serverAvailable} +
+

+ Detaillierte Analytics sind verfuegbar, wenn der uLoad-Server verbunden ist. +

+

+ Lokaler Click-Count: {link.clickCount} +

+
+ {:else} +

Noch keine Daten fuer diesen Zeitraum

+ {/if}
+ + + {#if serverAvailable} +
+ +
+

Geraete

+ {#if devices.length > 0} +
+ {#each devices as d} +
+
+ {d.deviceType || 'Unbekannt'} + + {Math.round((d.count / totalDevices) * 100)}% + +
+
+
+
+
+ {/each} +
+ {:else} +

Keine Daten

+ {/if} +
+ + +
+

Referrer

+ {#if referrers.length > 0} +
+ {#each referrers.slice(0, 8) as r} +
+ + {r.referer || 'Direkt'} + + {r.count} +
+ {/each} +
+ {:else} +

Keine Daten

+ {/if} +
+ + +
+

Laender

+ {#if countries.length > 0} +
+ {#each countries.slice(0, 8) as c} +
+
+ {c.country || 'Unbekannt'} + + {Math.round((c.count / totalCountries) * 100)}% + +
+
+
+
+
+ {/each} +
+ {:else} +

Keine Daten

+ {/if} +
+
+ {/if} {/if}
diff --git a/apps/manacore/apps/web/src/routes/(app)/uload/settings/+page.svelte b/apps/manacore/apps/web/src/routes/(app)/uload/settings/+page.svelte new file mode 100644 index 000000000..dc1d5afe6 --- /dev/null +++ b/apps/manacore/apps/web/src/routes/(app)/uload/settings/+page.svelte @@ -0,0 +1,106 @@ + + + + Settings - uLoad - ManaCore + + +
+
+ + + +

uLoad Einstellungen

+
+ + +
+

Daten

+
+
+

{links.value?.length ?? 0}

+

Links

+
+
+

{tags.value?.length ?? 0}

+

Tags

+
+
+

{folders.value?.length ?? 0}

+

Ordner

+
+
+
+ + +
+

Daten exportieren

+

Exportiere alle Links, Tags und Ordner als JSON-Datei.

+ +
+ + +
+

Gefahrenzone

+

+ Loescht alle lokalen uLoad-Daten (Links, Tags, Ordner). Synchronisierte Daten auf dem Server + bleiben erhalten. +

+ +
+
diff --git a/apps/manacore/apps/web/src/routes/(app)/uload/tags/+page.svelte b/apps/manacore/apps/web/src/routes/(app)/uload/tags/+page.svelte new file mode 100644 index 000000000..44e5c0bca --- /dev/null +++ b/apps/manacore/apps/web/src/routes/(app)/uload/tags/+page.svelte @@ -0,0 +1,164 @@ + + +
+
+
+ + + +

Tags

+
+ +
+ + {#if showCreateForm} +
+
+
+ + e.key === 'Enter' && createTag()} + /> +
+
+ + +
+ +
+
+ {/if} + + {#if !tags.value || tags.value.length === 0} +
+

Noch keine Tags

+

Erstelle Tags um deine Links zu organisieren.

+
+ {:else} +
+ {#each tags.value as tag (tag.id)} +
+ {#if editingTag?.id === tag.id} +
+ +
+ + + +
+
+ {:else} +
+
+ + {tag.name} +
+
+ {getUsageCount(tag.id)} Links + + +
+
+ {/if} +
+ {/each} +
+ {/if} +