From 7611d109be30b40bd9c440b33e2166c0a1b648b3 Mon Sep 17 00:00:00 2001 From: Till JS Date: Wed, 22 Apr 2026 14:12:18 +0200 Subject: [PATCH] feat(articles): M8 highlights view + stats + dashboard widget MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit useStats() live-query aggregates total / per-status / savedThisWeek / finishedThisWeek / topSites / totalHighlights in one scoped Dexie pass. useAllHighlights() joins cross-article highlights with article-header info (title, siteName, originalUrl) for rendering. /articles/highlights — HighlightsView groups chronologically-sorted highlights per article with color-accented stripes, click-to-reader jumps, and two export actions: - Copy as Markdown (clipboard) - Download .md (file) Export logic lives in lib/markdown-export.ts as a pure function (renderHighlightsMarkdown) so future snapshot tests don't need the render tree. Dashboard widget: ArticlesUnreadWidget mirrors NewsUnreadWidget's pattern — self-contained live query, top-3 unread/reading, stats strip ("N ungelesen · M diese Woche gespeichert"), empty state CTA to /articles/add. Registered in: - lib/types/dashboard.ts (WidgetType union + WIDGET_REGISTRY) - lib/components/dashboard/widget-registry.ts (component map) - lib/i18n/locales/dashboard/{de,en}.json (translations) fr/it/es intentionally left untranslated — consistent with how invoices_open and broadcasts are handled. ListView gains a pencil button next to the settings gear linking to /articles/highlights. Also: plan doc marks M7 + M8 done with commit refs; M1–M8 scope is now complete. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../components/dashboard/widget-registry.ts | 2 + .../src/lib/i18n/locales/dashboard/de.json | 4 + .../src/lib/i18n/locales/dashboard/en.json | 4 + .../src/lib/modules/articles/ListView.svelte | 9 + .../web/src/lib/modules/articles/index.ts | 4 + .../modules/articles/lib/markdown-export.ts | 68 ++++ .../web/src/lib/modules/articles/queries.ts | 143 +++++++++ .../articles/views/HighlightsView.svelte | 301 ++++++++++++++++++ .../widgets/ArticlesUnreadWidget.svelte | 78 +++++ apps/mana/apps/web/src/lib/types/dashboard.ts | 9 + .../(app)/articles/highlights/+page.svelte | 5 + docs/plans/articles-module.md | 6 +- 12 files changed, 631 insertions(+), 2 deletions(-) create mode 100644 apps/mana/apps/web/src/lib/modules/articles/lib/markdown-export.ts create mode 100644 apps/mana/apps/web/src/lib/modules/articles/views/HighlightsView.svelte create mode 100644 apps/mana/apps/web/src/lib/modules/articles/widgets/ArticlesUnreadWidget.svelte create mode 100644 apps/mana/apps/web/src/routes/(app)/articles/highlights/+page.svelte diff --git a/apps/mana/apps/web/src/lib/components/dashboard/widget-registry.ts b/apps/mana/apps/web/src/lib/components/dashboard/widget-registry.ts index 13a0a5769..535212b38 100644 --- a/apps/mana/apps/web/src/lib/components/dashboard/widget-registry.ts +++ b/apps/mana/apps/web/src/lib/components/dashboard/widget-registry.ts @@ -32,6 +32,7 @@ import NutritionProgressWidget from '$lib/modules/core/widgets/NutritionProgress import PlantWateringWidget from '$lib/modules/core/widgets/PlantWateringWidget.svelte'; import PeriodWidget from '$lib/modules/core/widgets/PeriodWidget.svelte'; import NewsUnreadWidget from '$lib/modules/news/widgets/NewsUnreadWidget.svelte'; +import ArticlesUnreadWidget from '$lib/modules/articles/widgets/ArticlesUnreadWidget.svelte'; import BodyStatsWidget from '$lib/modules/body/widgets/BodyStatsWidget.svelte'; import InvoicesOpenWidget from '$lib/modules/invoices/widgets/InvoicesOpenWidget.svelte'; import BroadcastsWidget from '$lib/modules/broadcast/widgets/BroadcastsWidget.svelte'; @@ -63,6 +64,7 @@ export const widgetComponents: Record = { 'activity-feed': ActivityFeedWidget, period: PeriodWidget, 'news-unread': NewsUnreadWidget, + 'articles-unread': ArticlesUnreadWidget, 'body-stats': BodyStatsWidget, 'invoices-open': InvoicesOpenWidget, broadcasts: BroadcastsWidget, diff --git a/apps/mana/apps/web/src/lib/i18n/locales/dashboard/de.json b/apps/mana/apps/web/src/lib/i18n/locales/dashboard/de.json index 32f7ed796..d5395af40 100644 --- a/apps/mana/apps/web/src/lib/i18n/locales/dashboard/de.json +++ b/apps/mana/apps/web/src/lib/i18n/locales/dashboard/de.json @@ -155,6 +155,10 @@ "title": "News", "description": "Top-Artikel aus deinem kuratierten Feed" }, + "articles_unread": { + "title": "Artikel", + "description": "Ungelesene Artikel aus deiner Leseliste" + }, "body_stats": { "title": "Body", "description": "Aktuelles Gewicht und Trainings-Status" diff --git a/apps/mana/apps/web/src/lib/i18n/locales/dashboard/en.json b/apps/mana/apps/web/src/lib/i18n/locales/dashboard/en.json index 52882b565..e87b3eb0d 100644 --- a/apps/mana/apps/web/src/lib/i18n/locales/dashboard/en.json +++ b/apps/mana/apps/web/src/lib/i18n/locales/dashboard/en.json @@ -155,6 +155,10 @@ "title": "News", "description": "Top articles from your curated feed" }, + "articles_unread": { + "title": "Articles", + "description": "Unread articles from your reading list" + }, "body_stats": { "title": "Body", "description": "Latest weight and training status" diff --git a/apps/mana/apps/web/src/lib/modules/articles/ListView.svelte b/apps/mana/apps/web/src/lib/modules/articles/ListView.svelte index bd5cb4cc9..52841d29f 100644 --- a/apps/mana/apps/web/src/lib/modules/articles/ListView.svelte +++ b/apps/mana/apps/web/src/lib/modules/articles/ListView.svelte @@ -80,6 +80,15 @@

Später lesen — gespeicherte Web-Artikel, offline verfügbar.

+ +
+ {#if rows.length > 0} +
+ + +
+ {/if} + + + {#if rows$.loading} +

Lädt…

+ {:else if groups.length === 0} +
+

Noch keine Highlights.

+

+ Markier eine Textstelle in einem gespeicherten Artikel — sie erscheint hier automatisch. +

+ +
+ {:else} +
+ {#each groups as group (group.articleId)} +
+
+ +
+
    + {#each group.highlights as h (h.id)} +
  • + + {#if h.note} +

    {h.note}

    + {/if} +
  • + {/each} +
+
+ {/each} +
+ {/if} + + + diff --git a/apps/mana/apps/web/src/lib/modules/articles/widgets/ArticlesUnreadWidget.svelte b/apps/mana/apps/web/src/lib/modules/articles/widgets/ArticlesUnreadWidget.svelte new file mode 100644 index 000000000..9ca1459c6 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/articles/widgets/ArticlesUnreadWidget.svelte @@ -0,0 +1,78 @@ + + +
+
+

+ + Artikel +

+ Alle → +
+ + {#if articles$.loading} +
+ {#each Array(3) as _} +
+ {/each} +
+ {:else if articles.length === 0} +
+

Noch keine Artikel gespeichert.

+ + Erste URL speichern + +
+ {:else if topUnread.length === 0} +
+

Alles gelesen — stark.

+ + Leseliste öffnen + +
+ {:else} + +
+ {stats.unread} ungelesen · {stats.savedThisWeek} diese Woche gespeichert +
+ {/if} +
diff --git a/apps/mana/apps/web/src/lib/types/dashboard.ts b/apps/mana/apps/web/src/lib/types/dashboard.ts index ea96ed334..6c23cb45e 100644 --- a/apps/mana/apps/web/src/lib/types/dashboard.ts +++ b/apps/mana/apps/web/src/lib/types/dashboard.ts @@ -32,6 +32,7 @@ export type WidgetType = | 'activity-feed' // TimeBlocks: recent activity across modules | 'period' // Period: current phase + days until next period | 'news-unread' // News: latest unread curated articles + | 'articles-unread' // Articles: saved read-it-later articles | 'body-stats' // Body: latest weight + active workout summary | 'invoices-open' // Invoices: open/overdue totals + oldest overdue | 'broadcasts'; // Broadcast: YTD counts + last sent + next scheduled @@ -355,6 +356,14 @@ export const WIDGET_REGISTRY: WidgetMeta[] = [ defaultSize: 'small', allowMultiple: false, }, + { + type: 'articles-unread', + nameKey: 'dashboard.widgets.articles_unread.title', + descriptionKey: 'dashboard.widgets.articles_unread.description', + icon: '📚', + defaultSize: 'small', + allowMultiple: false, + }, { type: 'body-stats', nameKey: 'dashboard.widgets.body_stats.title', diff --git a/apps/mana/apps/web/src/routes/(app)/articles/highlights/+page.svelte b/apps/mana/apps/web/src/routes/(app)/articles/highlights/+page.svelte new file mode 100644 index 000000000..cb03cd180 --- /dev/null +++ b/apps/mana/apps/web/src/routes/(app)/articles/highlights/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/docs/plans/articles-module.md b/docs/plans/articles-module.md index 20a175636..37fc71b16 100644 --- a/docs/plans/articles-module.md +++ b/docs/plans/articles-module.md @@ -16,11 +16,13 @@ **Hinweis AiProposalInbox:** Der apps/mana/CLAUDE.md-Abschnitt erwähnt `` als Inline-Mount, aber die Komponente existiert im aktuellen Codebase nicht — nach dem `pendingProposals`-Table-Drop in Dexie v29 wurde die Proposal-Darstellung auf `server-iteration-staging` + den Cross-Module-Inbox im Mission-Detail-View umgestellt. Articles-Proposals tauchen dort automatisch auf. Falls die Inline-Komponente wieder reaktiviert wird, muss nur der Mount in `ListView.svelte` ergänzt werden. -**M7 Share-Target + Bookmarklet: DONE** (commit pending) — `@mana/shared-pwa` bekommt neue Types (`PWAShareTarget`, `PWAShareTargetParams`), `createPWAConfig` threadet `shareTarget` in den Manifest, `ManifestConfig.share_target?` ergänzt. Web-App: `vite.config.ts` setzt `shareTarget: { action: '/articles/add', method: 'GET', params: { title, text, url } }`; `AddUrlForm` liest Query-Params in `onMount` (inkl. URL-Regex-Fallback auf `text` weil Chrome Android / WhatsApp den Link dort reinstecken), triggert auto-Vorschau. Neue Route `/articles/settings` rendert Bookmarklet-Karte (Drag-to-Bookmark + Copy-Snippet + expandable Quellcode) und Share-Target-Erklärung. `ListView` bekommt Zahnrad-Button zum Settings-Aufruf. +**M7 Share-Target + Bookmarklet: DONE** (commit `8a991f7c3`) — `@mana/shared-pwa` bekommt neue Types (`PWAShareTarget`, `PWAShareTargetParams`), `createPWAConfig` threadet `shareTarget` in den Manifest, `ManifestConfig.share_target?` ergänzt. Web-App: `vite.config.ts` setzt `shareTarget: { action: '/articles/add', method: 'GET', params: { title, text, url } }`; `AddUrlForm` liest Query-Params in `onMount` (inkl. URL-Regex-Fallback auf `text` weil Chrome Android / WhatsApp den Link dort reinstecken), triggert auto-Vorschau. Neue Route `/articles/settings` rendert Bookmarklet-Karte (Drag-to-Bookmark + Copy-Snippet + expandable Quellcode) und Share-Target-Erklärung. `ListView` bekommt Zahnrad-Button zum Settings-Aufruf. Nicht im Scope (bewusst ausgelassen): die „optional" im Plan markierte `_pendingUrls`-Offline-Queue. Kann als M7b nachgereicht werden wenn das Problem auftaucht. -Nächster Schritt: M8 (HighlightsView + Stats + Dashboard-Widget). +**M8 HighlightsView + Stats + Dashboard-Widget: DONE** (commit pending) — `queries.ts` bekommt `useStats()` (total, pro-Status, savedThisWeek, finishedThisWeek, topSites, totalHighlights) und `useAllHighlights()` (chronologisch, joined mit Artikel-Header-Info). Neue Route `/articles/highlights` mountet `HighlightsView.svelte`: Highlights pro Artikel gruppiert, farbige Akzent-Striche, Click-to-Reader, Copy-Markdown + Download-.md via `lib/markdown-export.ts` (pure Funktion, unit-testbar). Dashboard-Widget `ArticlesUnreadWidget` zeigt Top-3 unread + Stats-Strip; registriert in `widget-registry.ts`, `dashboard.ts` `WidgetType`-Union + `WIDGET_REGISTRY`; i18n-Keys in de.json + en.json (fr/it/es konsistent mit anderen Widgets weggelassen). ListView bekommt ✎-Button zum Highlights-Aufruf neben dem Settings-Zahnrad. + +Damit ist der komplette M1–M8-Scope aus diesem Plan umgesetzt. Phase-3-Kandidaten (Highlight→Note-Export mit Backlink, Full-Text-Search, Mercury/archive.org-Fallback, Jahresrückblick) bleiben offen für spätere Iterationen. ## Ziel