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 3f4ddbea5..effe32cee 100644 --- a/apps/mana/apps/web/src/lib/modules/articles/ListView.svelte +++ b/apps/mana/apps/web/src/lib/modules/articles/ListView.svelte @@ -1,17 +1,72 @@ @@ -172,6 +185,15 @@ {#if article.readingTimeMinutes}· {article.readingTimeMinutes} min{/if} {#if article.wordCount}· {article.wordCount} Wörter{/if} +
+ +
{ - const response = await fetchImpl(`${getManaApiUrl()}/api/v1/news/extract/save`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - ...(await authHeader()), - }, - body: JSON.stringify({ url }), - }); - if (!response.ok) { - const text = await response.text(); - throw new Error(`extractFromUrl failed: ${response.status} ${text}`); - } - return (await response.json()) as ExtractedArticleDto; -} +// Ad-hoc URL extraction moved to the `articles` module in M5 — see +// `modules/articles/api.ts` and `modules/articles/stores/articles.svelte.ts`. +// The `/api/v1/news/extract/*` routes in apps/api are kept for now as +// a legacy surface; the `news-research` module still relies on them. diff --git a/apps/mana/apps/web/src/lib/modules/news/index.ts b/apps/mana/apps/web/src/lib/modules/news/index.ts index 5bcb12abf..e041a5453 100644 --- a/apps/mana/apps/web/src/lib/modules/news/index.ts +++ b/apps/mana/apps/web/src/lib/modules/news/index.ts @@ -42,7 +42,7 @@ export { preferencesStore } from './stores/preferences.svelte'; export { reactionsStore } from './stores/reactions.svelte'; export { feedCacheStore } from './stores/feed-cache.svelte'; -export { fetchFeed, extractFromUrl } from './api'; +export { fetchFeed } from './api'; export type { FeedArticleDto, FeedQuery } from './api'; export { SOURCES_META, SOURCE_META_BY_SLUG, sourcesForTopic, TOPIC_LABELS } from './sources-meta'; diff --git a/apps/mana/apps/web/src/lib/modules/news/stores/articles.svelte.ts b/apps/mana/apps/web/src/lib/modules/news/stores/articles.svelte.ts index fdfba8ee5..6087d0382 100644 --- a/apps/mana/apps/web/src/lib/modules/news/stores/articles.svelte.ts +++ b/apps/mana/apps/web/src/lib/modules/news/stores/articles.svelte.ts @@ -1,12 +1,13 @@ /** * Articles store — the user's saved reading list. * - * Two paths in: - * - saveFromCurated(article) copies a row from the local pool - * mirror into the encrypted reading list. Used when the user - * hits "speichern" on a feed card. - * - saveFromUrl(url) hits POST /api/v1/news/extract/save and - * stores the result. Used by /news/add for ad-hoc URLs. + * Now single-purpose: saveFromCurated copies a row from the local pool + * mirror into the encrypted reading list (hit when the user presses + * "speichern" on a feed card). The ad-hoc URL path (`saveFromUrl` + + * the `type: 'saved'` discriminator) moved to the Articles module in + * M5 — see `modules/articles/migrations/from-news.ts` for the one-off + * data migration and `modules/articles/stores/articles.svelte.ts` for + * the replacement flow. * * All other operations (read/archive/favorite/delete) are plain * updates against `newsArticles`. @@ -15,7 +16,6 @@ import { encryptRecord } from '$lib/data/crypto'; import { emitDomainEvent } from '$lib/data/events'; import { articleTable } from '../collections'; -import { extractFromUrl } from '../api'; import { toArticle } from '../queries'; import type { Article, LocalArticle, LocalCachedArticle } from '../types'; @@ -58,35 +58,6 @@ export const articlesStore = { return snapshot; }, - async saveFromUrl(url: string): Promise
{ - const extracted = await extractFromUrl(url); - const newLocal: LocalArticle = { - id: crypto.randomUUID(), - type: 'saved', - sourceCuratedId: null, - originalUrl: extracted.originalUrl, - title: extracted.title, - excerpt: extracted.excerpt, - content: extracted.content, - htmlContent: extracted.htmlContent, - author: extracted.author, - siteName: extracted.siteName, - sourceSlug: null, - imageUrl: null, - categoryId: null, - wordCount: extracted.wordCount, - readingTimeMinutes: extracted.readingTimeMinutes, - publishedAt: null, - isArchived: false, - isRead: false, - isFavorite: false, - }; - const snapshot = toArticle(newLocal); - await encryptRecord('newsArticles', newLocal); - await articleTable.add(newLocal); - return snapshot; - }, - async markRead(id: string, isRead = true): Promise { await articleTable.update(id, { isRead, diff --git a/apps/mana/apps/web/src/lib/modules/news/tools.ts b/apps/mana/apps/web/src/lib/modules/news/tools.ts index 4ffef8e99..99be9aa19 100644 --- a/apps/mana/apps/web/src/lib/modules/news/tools.ts +++ b/apps/mana/apps/web/src/lib/modules/news/tools.ts @@ -2,15 +2,19 @@ * News Tools — LLM-accessible operations for the news module. * * `save_news_article` is the agent's path into the user's reading list. - * On approve, the executor calls `articlesStore.saveFromUrl(url)` which - * routes through `apps/api /api/v1/news/extract/save` (Readability) and - * stores the encrypted result in `newsArticles`. `title` and `summary` - * are display hints — the canonical title/excerpt come back from the - * extractor so the AI can't lie about content. + * M5 moved the saved-article storage to the `articles` module; this + * tool now routes through `articlesStore.saveFromUrl(url)` there. The + * tool name stays `save_news_article` because historic AI mission + * iterations in the DB reference it — renaming would break the audit + * trail. A future `save_article` can be added as an alias in M6. + * + * `title` and `summary` are display hints for the approval dialog — + * the canonical title/excerpt come from the extractor so the AI can't + * lie about content. */ import type { ModuleTool } from '$lib/data/tools/types'; -import { articlesStore } from './stores/articles.svelte'; +import { articlesStore } from '$lib/modules/articles/stores/articles.svelte'; export const newsTools: ModuleTool[] = [ { @@ -35,11 +39,13 @@ export const newsTools: ModuleTool[] = [ ], async execute(params) { const url = params.url as string; - const article = await articlesStore.saveFromUrl(url); + const { article, duplicate } = await articlesStore.saveFromUrl(url); return { success: true, - message: `Artikel gespeichert: ${article.title}`, - data: { articleId: article.id, title: article.title }, + message: duplicate + ? `Artikel bereits gespeichert: ${article.title}` + : `Artikel gespeichert: ${article.title}`, + data: { articleId: article.id, title: article.title, duplicate }, }; }, }, diff --git a/apps/mana/apps/web/src/routes/(app)/+layout.svelte b/apps/mana/apps/web/src/routes/(app)/+layout.svelte index 64d082533..a75c1a70d 100644 --- a/apps/mana/apps/web/src/routes/(app)/+layout.svelte +++ b/apps/mana/apps/web/src/routes/(app)/+layout.svelte @@ -8,6 +8,7 @@ import { todoReminderSource } from '$lib/modules/todo/reminder-source'; import { startEventStore, stopEventStore } from '$lib/data/events/event-store'; import { startMissionTick, stopMissionTick } from '$lib/data/ai/missions/setup'; + import { runArticlesFromNewsMigration } from '$lib/modules/articles/migrations/from-news'; import { startServerIterationExecutor, stopServerIterationExecutor, @@ -546,6 +547,10 @@ // Apply server-planned iterations locally on sync — see // data/ai/missions/server-iteration-executor.ts. startServerIterationExecutor(); + // One-off migration: legacy news `type='saved'` rows → new + // articles module. Sentinel-gated so it runs once per device. + // See modules/articles/migrations/from-news.ts. + void runArticlesFromNewsMigration(); }); // Restore nav collapsed state (cheap, keep inline) diff --git a/apps/mana/apps/web/src/routes/(app)/news-research/+page.svelte b/apps/mana/apps/web/src/routes/(app)/news-research/+page.svelte index 7d41b95cb..cd361ce20 100644 --- a/apps/mana/apps/web/src/routes/(app)/news-research/+page.svelte +++ b/apps/mana/apps/web/src/routes/(app)/news-research/+page.svelte @@ -8,7 +8,7 @@ - - URL hinzufügen — News — Mana - - -
-
- -

Artikel speichern

-

- Füge eine URL ein. Wir extrahieren den Volltext (Mozilla Readability) und legen ihn in deine - verschlüsselte Leseliste. -

-
- -
- - - -
- - {#if error} -
{error}
- {/if} -
+

Verschoben nach /articles/add

diff --git a/apps/mana/apps/web/src/routes/(app)/news/saved/+page.svelte b/apps/mana/apps/web/src/routes/(app)/news/saved/+page.svelte index adcbcbb7b..af8127534 100644 --- a/apps/mana/apps/web/src/routes/(app)/news/saved/+page.svelte +++ b/apps/mana/apps/web/src/routes/(app)/news/saved/+page.svelte @@ -1,709 +1,23 @@ - - Gespeichert — News — Mana - - -
-
-
- -

Gespeichert

-
- + URL hinzufügen -
- - - - -
-
- - {#each categories as cat (cat.id)} - - {/each} - -
- {#if showCategoryEditor} -
-
{ - e.preventDefault(); - void createCategory(); - }} - > - - -
- {#if categories.length > 0} -
    - {#each categories as cat (cat.id)} -
  • - - {cat.name} - - -
  • - {/each} -
- {:else} -

Noch keine Kategorien. Erstelle eine oben.

- {/if} -
- {/if} -
- - {#if visible.length === 0} -
- {#if tab === 'unread'} -

Keine ungelesenen Artikel.

-

Reagiere im Feed mit „❤️ Interessiert" um Artikel hier zu sammeln.

- {:else if tab === 'favorites'} -

Noch keine Favoriten.

- {:else} -

Archiv ist leer.

- {/if} -
- {:else} -
- {#each visible as article (article.id)} -
- {#if article.imageUrl} - - {/if} -
-
- {article.siteName ?? 'Eigener Link'} - {#if article.publishedAt} - · - {formatRelativeTime(article.publishedAt)} - {/if} - {#if article.readingTimeMinutes} - · - {article.readingTimeMinutes} min - {/if} - {#if article.type === 'saved'} - eigen - {/if} -
- - {#if article.excerpt} -

{article.excerpt}

- {/if} -
-
- - - {#if tab === 'archive'} - - {:else} - - {/if} - -
-
- {/each} -
- {/if} -
+

Verschoben nach /articles