From 609f66253875c1d807638329d8fce89aa1c64114 Mon Sep 17 00:00:00 2001 From: Till JS Date: Mon, 18 May 2026 15:03:56 +0200 Subject: [PATCH] chore(mana): news aus unified-App entfernen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reader-Surface ist nach Pageta (pageta.mana.how + pageta-api.mana.how) umgezogen, das seit 2026-05-16 live ist und mehr Features bietet als das alte managarten-news-Modul: - Highlights (4 Farben, plain-text-offsets, Kontext) - Reading-Progress + User-Note pro Artikel - Bulk-Import (200 URLs/Job mit Worker) - 5 MCP-Tools (save/list/archive/tag/highlight) - Reading-Status-Enum (unread/reading/finished/archived) statt Boolean Was Pageta NICHT hat: Categories mit Color+Icon — Pageta verwendet freie String-Tags statt visuelle Folders. Bewusste Design-Entscheidung in Pageta. Daten-Migration: KEIN automatisches Skript. User mit gespeicherten Artikeln im managarten-newsArticles müssen ihre Liste in Pageta neu aufbauen (oder Bulk-Import via /api/v1/imports verwenden). Gelöscht / abgebaut: - Module: apps/mana/.../modules/news + Routen + Locales - apps/articles/migrations/from-news.ts (one-off-Migration nach articles-Modul, Sentinel-gated, abgeschlossen) + Call in (app)/+layout.svelte - apps/api/src/modules/news + MCP-Executor save_news_article - shared-branding: APP_ICONS.news + MANA_APPS news-Entry - shared-ai/tools/schemas save_news_article - shared-types/spaces: 3 'news'-Einträge in Space-Modul-Listen - Cross-Module: news-research/ListView + (app)/news-research/+page.svelte hatten den preferencesStore + usePreferences vom news-Modul für Custom-Feed-Pinning — Pin-UI entfernt (Custom-Feeds sind jetzt Pageta-Verantwortung) - Dashboard: 'news-unread' Widget + NewsUnreadWidget-Import - Registries: app-registry/apps.ts (News registerApp + Newspaper icon + Header), categories, help-content, module-registry, data/tools/init - i18n: news in apps/{de,en,es,fr,it}.json Was BLEIBT: - `news-research` Modul + `apps/api/src/modules/news-research/` — RSS-Discovery + Search-Funktion bleibt im managarten als Recherche-Tool für andere Module - `mana-news-pool` Plattform-Service (Code/mana/services/) — wird von news-research + Pageta-Standalone konsumiert - shared-ai `research_news` Tool Dexie v65 Migration: - droppt newsArticles, newsCategories, newsPreferences, newsReactions, newsCachedFeed mana-web svelte-check 0/0, snapshot test 10/10. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/api/src/index.ts | 2 - apps/api/src/mcp/executor.ts | 27 - apps/api/src/modules/news/routes.ts | 94 --- .../apps/web/src/lib/app-registry/apps.ts | 24 +- .../web/src/lib/app-registry/help-content.ts | 15 - .../components/dashboard/widget-registry.ts | 2 - apps/mana/apps/web/src/lib/data/database.ts | 16 + .../web/src/lib/data/module-registry.test.ts | 7 - .../apps/web/src/lib/data/module-registry.ts | 2 - apps/mana/apps/web/src/lib/data/tools/init.ts | 2 - .../web/src/lib/i18n/locales/apps/de.json | 1 - .../web/src/lib/i18n/locales/apps/en.json | 1 - .../web/src/lib/i18n/locales/apps/es.json | 1 - .../web/src/lib/i18n/locales/apps/fr.json | 1 - .../web/src/lib/i18n/locales/apps/it.json | 1 - .../web/src/lib/i18n/locales/news/de.json | 149 ---- .../web/src/lib/i18n/locales/news/en.json | 149 ---- .../web/src/lib/i18n/locales/news/es.json | 149 ---- .../web/src/lib/i18n/locales/news/fr.json | 149 ---- .../web/src/lib/i18n/locales/news/it.json | 149 ---- .../modules/articles/migrations/from-news.ts | 164 ---- .../lib/modules/news-research/ListView.svelte | 33 - .../web/src/lib/modules/news/ListView.svelte | 369 --------- .../mana/apps/web/src/lib/modules/news/api.ts | 89 -- .../web/src/lib/modules/news/collections.ts | 35 - .../web/src/lib/modules/news/feed-engine.ts | 204 ----- .../apps/web/src/lib/modules/news/index.ts | 65 -- .../web/src/lib/modules/news/module.config.ts | 19 - .../apps/web/src/lib/modules/news/queries.ts | 182 ---- .../web/src/lib/modules/news/sources-meta.ts | 83 -- .../modules/news/stores/articles.svelte.ts | 92 --- .../modules/news/stores/categories.svelte.ts | 89 -- .../modules/news/stores/feed-cache.svelte.ts | 120 --- .../modules/news/stores/preferences.svelte.ts | 137 --- .../modules/news/stores/reactions.svelte.ts | 61 -- .../apps/web/src/lib/modules/news/tools.ts | 52 -- .../apps/web/src/lib/modules/news/types.ts | 198 ----- .../news/widgets/NewsUnreadWidget.svelte | 129 --- apps/mana/apps/web/src/lib/types/dashboard.ts | 9 - .../apps/web/src/routes/(app)/+layout.svelte | 5 - .../routes/(app)/news-research/+page.svelte | 43 - .../web/src/routes/(app)/news/+layout.svelte | 39 - .../web/src/routes/(app)/news/+page.svelte | 777 ------------------ .../src/routes/(app)/news/[id]/+page.svelte | 334 -------- .../src/routes/(app)/news/add/+page.svelte | 26 - .../(app)/news/preferences/+page.svelte | 242 ------ .../src/routes/(app)/news/saved/+page.svelte | 26 - .../routes/(app)/news/sources/+page.svelte | 168 ---- docker-compose.macmini.yml | 6 +- docker/prometheus/prometheus.yml | 1 - packages/shared-ai/src/tools/schemas.ts | 24 - packages/shared-branding/src/app-icons.ts | 3 - packages/shared-branding/src/mana-apps.ts | 17 - packages/shared-types/src/spaces.ts | 3 - 54 files changed, 19 insertions(+), 4766 deletions(-) delete mode 100644 apps/api/src/modules/news/routes.ts delete mode 100644 apps/mana/apps/web/src/lib/i18n/locales/news/de.json delete mode 100644 apps/mana/apps/web/src/lib/i18n/locales/news/en.json delete mode 100644 apps/mana/apps/web/src/lib/i18n/locales/news/es.json delete mode 100644 apps/mana/apps/web/src/lib/i18n/locales/news/fr.json delete mode 100644 apps/mana/apps/web/src/lib/i18n/locales/news/it.json delete mode 100644 apps/mana/apps/web/src/lib/modules/articles/migrations/from-news.ts delete mode 100644 apps/mana/apps/web/src/lib/modules/news/ListView.svelte delete mode 100644 apps/mana/apps/web/src/lib/modules/news/api.ts delete mode 100644 apps/mana/apps/web/src/lib/modules/news/collections.ts delete mode 100644 apps/mana/apps/web/src/lib/modules/news/feed-engine.ts delete mode 100644 apps/mana/apps/web/src/lib/modules/news/index.ts delete mode 100644 apps/mana/apps/web/src/lib/modules/news/module.config.ts delete mode 100644 apps/mana/apps/web/src/lib/modules/news/queries.ts delete mode 100644 apps/mana/apps/web/src/lib/modules/news/sources-meta.ts delete mode 100644 apps/mana/apps/web/src/lib/modules/news/stores/articles.svelte.ts delete mode 100644 apps/mana/apps/web/src/lib/modules/news/stores/categories.svelte.ts delete mode 100644 apps/mana/apps/web/src/lib/modules/news/stores/feed-cache.svelte.ts delete mode 100644 apps/mana/apps/web/src/lib/modules/news/stores/preferences.svelte.ts delete mode 100644 apps/mana/apps/web/src/lib/modules/news/stores/reactions.svelte.ts delete mode 100644 apps/mana/apps/web/src/lib/modules/news/tools.ts delete mode 100644 apps/mana/apps/web/src/lib/modules/news/types.ts delete mode 100644 apps/mana/apps/web/src/lib/modules/news/widgets/NewsUnreadWidget.svelte delete mode 100644 apps/mana/apps/web/src/routes/(app)/news/+layout.svelte delete mode 100644 apps/mana/apps/web/src/routes/(app)/news/+page.svelte delete mode 100644 apps/mana/apps/web/src/routes/(app)/news/[id]/+page.svelte delete mode 100644 apps/mana/apps/web/src/routes/(app)/news/add/+page.svelte delete mode 100644 apps/mana/apps/web/src/routes/(app)/news/preferences/+page.svelte delete mode 100644 apps/mana/apps/web/src/routes/(app)/news/saved/+page.svelte delete mode 100644 apps/mana/apps/web/src/routes/(app)/news/sources/+page.svelte diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index a88e912a7..5ad6a3a44 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -34,7 +34,6 @@ import { profileRoutes } from './modules/profile/routes'; import { storageRoutes } from './modules/storage/routes'; import { todoRoutes } from './modules/todo/routes'; import { guidesRoutes } from './modules/guides/routes'; -import { newsRoutes } from './modules/news/routes'; import { newsResearchRoutes } from './modules/news-research/routes'; import { articlesRoutes } from './modules/articles/routes'; import { startArticleImportWorker } from './modules/articles/import-worker'; @@ -130,7 +129,6 @@ app.route('/api/v1/profile', profileRoutes); app.route('/api/v1/storage', storageRoutes); app.route('/api/v1/todo', todoRoutes); app.route('/api/v1/guides', guidesRoutes); -app.route('/api/v1/news', newsRoutes); app.route('/api/v1/news-research', newsResearchRoutes); app.route('/api/v1/articles', articlesRoutes); app.route('/api/v1/traces', tracesRoutes); diff --git a/apps/api/src/mcp/executor.ts b/apps/api/src/mcp/executor.ts index 321ebf277..1e7422ae8 100644 --- a/apps/api/src/mcp/executor.ts +++ b/apps/api/src/mcp/executor.ts @@ -616,33 +616,6 @@ register('log_habit', async (args, userId) => { return ok(`Habit geloggt.`); }); -// ── News tools ──────────────────────────────────────────────── - -register('save_news_article', async (args, userId) => { - const articleId = crypto.randomUUID(); - const now = nowIso(); - const data = { - id: articleId, - userId, - url: args.url as string, - title: (args.title as string) ?? '', - summary: (args.summary as string) ?? '', - savedAt: now, - createdAt: now, - updatedAt: now, - }; - await writeRecord( - userId, - 'news', - 'savedArticles', - articleId, - 'insert', - data, - fieldTs(Object.keys(data)) - ); - return ok(`Artikel gespeichert: "${args.title || args.url}"`, { id: articleId }); -}); - // ── Entry point ──────────────────────────────────────────────── /** diff --git a/apps/api/src/modules/news/routes.ts b/apps/api/src/modules/news/routes.ts deleted file mode 100644 index 6702af7b3..000000000 --- a/apps/api/src/modules/news/routes.ts +++ /dev/null @@ -1,94 +0,0 @@ -/** - * News module — Reads the curated article pool + extracts ad-hoc URLs. - * - * Pool population: handled by the Plattform-Service `mana-news-pool` - * (Port 3079, eigene DB `mana_news_pool`, Schema `pool.curated_articles`). - * Cutover am 2026-05-17: ehemals direkter Raw-SQL-Read auf - * `mana_platform.news.curated_articles` aus dem `news-ingester:3066`- - * Container. Hier nur noch HTTP-Proxy auf den Plattform-Pool. - * - * Saved articles (die persönliche Reading-List eines Users) leben - * weiterhin client-side in der IndexedDB der unified Mana-App und - * syncen via mana-sync; dieses Modul sieht sie nicht. - */ - -import { Hono } from 'hono'; -import { extractFromUrl } from '@mana/shared-rss'; - -const POOL_URL = process.env.MANA_NEWS_POOL_URL ?? 'http://mana-news-pool:3079'; -const POOL_KEY = process.env.MANA_SERVICE_KEY ?? ''; - -// ─── Routes ───────────────────────────────────────────────── - -const routes = new Hono(); - -// ─── Feed (proxy on mana-news-pool) ──────────────────────── -// -// Query params: -// topics — comma-separated topic slugs (tech,wissenschaft,…) -// lang — 'de' | 'en' | 'all' (default 'all') -// since — ISO timestamp -// limit — default 50, max 200 -// offset — default 0 - -routes.get('/feed', async (c) => { - const passthrough = ['topics', 'lang', 'since', 'limit', 'offset'] as const; - const url = new URL(`${POOL_URL}/feed`); - for (const k of passthrough) { - const v = c.req.query(k); - if (v) url.searchParams.set(k, v); - } - - try { - const res = await fetch(url.toString(), { - headers: { 'X-Service-Key': POOL_KEY }, - signal: AbortSignal.timeout(8_000), - }); - if (!res.ok) { - console.warn(`[news] pool ${url} → ${res.status}`); - return c.json([] as Record[]); - } - const data = (await res.json()) as Record[]; - return c.json(data); - } catch (err) { - console.warn('[news] pool fetch failed', err); - return c.json([] as Record[]); - } -}); - -// ─── Extract (content extraction for user-pasted URLs) ───── - -routes.post('/extract/preview', async (c) => { - const { url } = await c.req.json<{ url: string }>(); - if (!url) return c.json({ error: 'URL is required' }, 400); - - const article = await extractFromUrl(url); - if (!article) return c.json({ error: 'Extraction failed' }, 502); - return c.json(article); -}); - -routes.post('/extract/save', async (c) => { - const { url } = await c.req.json<{ url: string }>(); - if (!url) return c.json({ error: 'URL is required' }, 400); - - const extracted = await extractFromUrl(url); - if (!extracted) return c.json({ error: 'Extraction failed' }, 502); - - return c.json({ - id: crypto.randomUUID(), - type: 'saved', - sourceOrigin: 'user_saved', - originalUrl: url, - title: extracted.title, - content: extracted.content, - htmlContent: extracted.htmlContent, - excerpt: extracted.excerpt, - author: extracted.byline, - siteName: extracted.siteName, - wordCount: extracted.wordCount, - readingTimeMinutes: extracted.readingTimeMinutes, - isArchived: false, - }); -}); - -export { routes as newsRoutes }; diff --git a/apps/mana/apps/web/src/lib/app-registry/apps.ts b/apps/mana/apps/web/src/lib/app-registry/apps.ts index 8bbc8ef17..e1c5f601d 100644 --- a/apps/mana/apps/web/src/lib/app-registry/apps.ts +++ b/apps/mana/apps/web/src/lib/app-registry/apps.ts @@ -36,7 +36,6 @@ import { Calculator, Lightning, PencilRuler, - Newspaper, Person, GenderFemale, CalendarStar, @@ -96,7 +95,7 @@ import { // Daily-use: habits · notes · journal · myday · drink · // mood · sleep · activity · times · finance // Knowledge: chat · kontext · cards · quiz · guides · -// news · news-research · research-lab · articles · +// news-research · research-lab · articles · // library · writing · comic · presi // Body & life: body · meditate · stretch · period · // dreams · firsts · lasts · habits · recipes @@ -755,27 +754,6 @@ registerApp({ paramKey: 'eventId', }); -registerApp({ - id: 'news', - name: 'News', - color: '#10B981', - icon: Newspaper, - views: { - list: { load: () => import('$lib/modules/news/ListView.svelte') }, - }, - contextMenuActions: [ - { - id: 'open-feed', - label: 'Feed öffnen', - icon: Plus, - action: () => - window.dispatchEvent( - new CustomEvent('mana:quick-action', { detail: { app: 'news', action: 'open' } }) - ), - }, - ], -}); - registerApp({ id: 'news-research', name: 'News Research', diff --git a/apps/mana/apps/web/src/lib/app-registry/help-content.ts b/apps/mana/apps/web/src/lib/app-registry/help-content.ts index 859722cec..200bdfd99 100644 --- a/apps/mana/apps/web/src/lib/app-registry/help-content.ts +++ b/apps/mana/apps/web/src/lib/app-registry/help-content.ts @@ -816,21 +816,6 @@ export const MODULE_HELP: Record = { ], tips: ['Jedes Modul hat zusätzlich sein eigenes ?-Icon mit kontextueller Hilfe'], }, - news: { - description: - 'Kuratierter News-Feed aus vertrauenswürdigen Quellen. 7 Themen (Tech, Wissenschaft, Weltgeschehen, Wirtschaft, Kultur, Gesundheit, Politik). Alle 15 Minuten aktualisiert.', - features: [ - 'Thema-Filter + Sprachauswahl (DE/EN)', - 'Eigene Abos über Custom-Feeds', - 'Artikel in Leseliste speichern (verschlüsselt)', - 'AI-Tool: `save_news_article`', - 'Schwester-Modul: /research-lab für tiefere Recherche', - ], - tips: [ - 'Ziehe einen Artikel auf Notizen um eine Zusammenfassung zu erstellen', - 'Für Nicht-RSS-Quellen: Research Lab mit Deep-Research nutzen', - ], - }, profile: { description: 'Dein persönliches Profil — der Kontext-Doc, den alle AI-Agents als Basis nutzen. Wer bist du, was willst du, was sollte Mana über dich wissen.', 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 83e6ebf29..7cfe975d5 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 @@ -27,7 +27,6 @@ import PresiDecksWidget from './widgets/PresiDecksWidget.svelte'; import RecentContactsWidget from '$lib/modules/core/widgets/RecentContactsWidget.svelte'; import ActiveTimerWidget from '$lib/modules/core/widgets/ActiveTimerWidget.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'; @@ -56,7 +55,6 @@ export const widgetComponents: Record = { 'day-timeline': DayTimelineWidget, 'activity-feed': ActivityFeedWidget, period: PeriodWidget, - 'news-unread': NewsUnreadWidget, 'articles-unread': ArticlesUnreadWidget, 'body-stats': BodyStatsWidget, 'invoices-open': InvoicesOpenWidget, diff --git a/apps/mana/apps/web/src/lib/data/database.ts b/apps/mana/apps/web/src/lib/data/database.ts index 5675cb0da..3fc8d445d 100644 --- a/apps/mana/apps/web/src/lib/data/database.ts +++ b/apps/mana/apps/web/src/lib/data/database.ts @@ -1575,6 +1575,22 @@ db.version(64).stores({ memoSpaces: null, }); +// v65 — News module retirement (2026-05-18). +// News-Reader-Surface ist nach Pageta (pageta.mana.how) umgezogen, +// das mit eigener Postgres-DB + eigenem Article-Store läuft. Der +// kuratierte Pool selbst lebt im Plattform-Service `mana-news-pool` +// und wird via news-research (in managarten) + Pageta-Reader (extern) +// weiterhin genutzt. +// dropped: newsArticles, newsCategories, newsPreferences, newsReactions, +// newsCachedFeed (war NICHT synced, nur local-mirror). +db.version(65).stores({ + newsArticles: null, + newsCategories: null, + newsPreferences: null, + newsReactions: null, + newsCachedFeed: null, +}); + // ─── Sync Routing ────────────────────────────────────────── // SYNC_APP_MAP, TABLE_TO_SYNC_NAME, TABLE_TO_APP, SYNC_NAME_TO_TABLE, // toSyncName() and fromSyncName() are now derived from per-module diff --git a/apps/mana/apps/web/src/lib/data/module-registry.test.ts b/apps/mana/apps/web/src/lib/data/module-registry.test.ts index 6c59bb9de..f6bff8a96 100644 --- a/apps/mana/apps/web/src/lib/data/module-registry.test.ts +++ b/apps/mana/apps/web/src/lib/data/module-registry.test.ts @@ -57,8 +57,6 @@ const INTERNAL_TABLES = new Set([ // which writes via its module's sync path — proposals themselves never // leave the device. 'pendingProposals', - // Local-only news feed cache. - 'newsCachedFeed', ]); // ─── Dexie tables that survive in the schema for backwards-compat with @@ -232,7 +230,6 @@ describe('module-registry — snapshot', () => { finance: ['transactions', 'financeCategories', 'budgets'], places: ['places', 'locationLogs', 'placeTags'], playground: ['playgroundSnippets', 'playgroundConversations', 'playgroundMessages'], - news: ['newsArticles', 'newsCategories', 'newsPreferences', 'newsReactions'], body: [ 'bodyExercises', 'bodyRoutines', @@ -315,10 +312,6 @@ describe('module-registry — snapshot', () => { playgroundSnippets: 'snippets', playgroundConversations: 'conversations', playgroundMessages: 'messages', - newsArticles: 'articles', - newsCategories: 'categories', - newsPreferences: 'preferences', - newsReactions: 'reactions', quizQuestions: 'questions', quizAttempts: 'attempts', articleHighlights: 'highlights', diff --git a/apps/mana/apps/web/src/lib/data/module-registry.ts b/apps/mana/apps/web/src/lib/data/module-registry.ts index 6ff0c7c10..6413db838 100644 --- a/apps/mana/apps/web/src/lib/data/module-registry.ts +++ b/apps/mana/apps/web/src/lib/data/module-registry.ts @@ -77,7 +77,6 @@ import { eventsModuleConfig } from '$lib/modules/events/module.config'; import { financeModuleConfig } from '$lib/modules/finance/module.config'; import { placesModuleConfig } from '$lib/modules/places/module.config'; import { playgroundModuleConfig } from '$lib/modules/playground/module.config'; -import { newsModuleConfig } from '$lib/modules/news/module.config'; import { bodyModuleConfig } from '$lib/modules/body/module.config'; import { firstsModuleConfig } from '$lib/modules/firsts/module.config'; import { lastsModuleConfig } from '$lib/modules/lasts/module.config'; @@ -133,7 +132,6 @@ export const MODULE_CONFIGS: readonly ModuleConfig[] = [ financeModuleConfig, placesModuleConfig, playgroundModuleConfig, - newsModuleConfig, bodyModuleConfig, firstsModuleConfig, lastsModuleConfig, diff --git a/apps/mana/apps/web/src/lib/data/tools/init.ts b/apps/mana/apps/web/src/lib/data/tools/init.ts index 41251551d..347f5c86a 100644 --- a/apps/mana/apps/web/src/lib/data/tools/init.ts +++ b/apps/mana/apps/web/src/lib/data/tools/init.ts @@ -26,7 +26,6 @@ import { firstsTools } from '$lib/modules/firsts/tools'; import { lastsTools } from '$lib/modules/lasts/tools'; import { guidesTools } from '$lib/modules/guides/tools'; import { inventoryTools } from '$lib/modules/inventory/tools'; -import { newsTools } from '$lib/modules/news/tools'; import { newsResearchTools } from '$lib/modules/news-research/tools'; import { articlesTools } from '$lib/modules/articles/tools'; import { recipesTools } from '$lib/modules/recipes/tools'; @@ -74,7 +73,6 @@ export function initTools(): void { registerTools(lastsTools); registerTools(guidesTools); registerTools(inventoryTools); - registerTools(newsTools); registerTools(newsResearchTools); registerTools(articlesTools); registerTools(recipesTools); diff --git a/apps/mana/apps/web/src/lib/i18n/locales/apps/de.json b/apps/mana/apps/web/src/lib/i18n/locales/apps/de.json index 5e18b3342..01fb8328e 100644 --- a/apps/mana/apps/web/src/lib/i18n/locales/apps/de.json +++ b/apps/mana/apps/web/src/lib/i18n/locales/apps/de.json @@ -30,7 +30,6 @@ "automations": "Automationen", "playground": "Playground", "kontext": "Web-Kontext", - "news": "News", "news-research": "News-Recherche", "articles": "Artikel", "research-lab": "Recherche-Labor", diff --git a/apps/mana/apps/web/src/lib/i18n/locales/apps/en.json b/apps/mana/apps/web/src/lib/i18n/locales/apps/en.json index 6a9f2579a..feffff50d 100644 --- a/apps/mana/apps/web/src/lib/i18n/locales/apps/en.json +++ b/apps/mana/apps/web/src/lib/i18n/locales/apps/en.json @@ -30,7 +30,6 @@ "automations": "Automations", "playground": "Playground", "kontext": "Web Context", - "news": "News", "news-research": "News Research", "articles": "Articles", "research-lab": "Research Lab", diff --git a/apps/mana/apps/web/src/lib/i18n/locales/apps/es.json b/apps/mana/apps/web/src/lib/i18n/locales/apps/es.json index b4ce3114f..5517c2cda 100644 --- a/apps/mana/apps/web/src/lib/i18n/locales/apps/es.json +++ b/apps/mana/apps/web/src/lib/i18n/locales/apps/es.json @@ -30,7 +30,6 @@ "automations": "Automatizaciones", "playground": "Playground", "kontext": "Contexto web", - "news": "Noticias", "news-research": "Investigación de noticias", "articles": "Artículos", "research-lab": "Laboratorio de investigación", diff --git a/apps/mana/apps/web/src/lib/i18n/locales/apps/fr.json b/apps/mana/apps/web/src/lib/i18n/locales/apps/fr.json index deda7bbc8..c422b6f5f 100644 --- a/apps/mana/apps/web/src/lib/i18n/locales/apps/fr.json +++ b/apps/mana/apps/web/src/lib/i18n/locales/apps/fr.json @@ -30,7 +30,6 @@ "automations": "Automations", "playground": "Playground", "kontext": "Contexte web", - "news": "Actualités", "news-research": "Recherche d'actus", "articles": "Articles", "research-lab": "Laboratoire de recherche", diff --git a/apps/mana/apps/web/src/lib/i18n/locales/apps/it.json b/apps/mana/apps/web/src/lib/i18n/locales/apps/it.json index 09a59a702..d2e6d3a21 100644 --- a/apps/mana/apps/web/src/lib/i18n/locales/apps/it.json +++ b/apps/mana/apps/web/src/lib/i18n/locales/apps/it.json @@ -30,7 +30,6 @@ "automations": "Automazioni", "playground": "Playground", "kontext": "Contesto Web", - "news": "News", "news-research": "Ricerca News", "articles": "Articoli", "research-lab": "Laboratorio di ricerca", diff --git a/apps/mana/apps/web/src/lib/i18n/locales/news/de.json b/apps/mana/apps/web/src/lib/i18n/locales/news/de.json deleted file mode 100644 index e19c93852..000000000 --- a/apps/mana/apps/web/src/lib/i18n/locales/news/de.json +++ /dev/null @@ -1,149 +0,0 @@ -{ - "app": { - "name": "News", - "tagline": "Dein kuratierter Newsfeed" - }, - "feed": { - "title": "News", - "articles": "{count} Artikel", - "refresh": "Neu laden", - "loading": "Lade Artikel…", - "empty": "Keine neuen Artikel zu deinen Themen.", - "emptyHint": "Probiere ↻ oder erweitere deine Themen.", - "loadError": "Fehler beim Laden", - "savedLink": "Gespeichert", - "settingsLink": "Einstellungen", - "openArticleAria": "Artikel öffnen", - "savedBadgeTitle": "In deiner Leseliste", - "savedBadgeText": "❤️ gespeichert", - "readingTimeMin": "{n} min" - }, - "reactions": { - "interested": "Interessiert", - "interestedSaved": "Gespeichert", - "interestedTitle": "Speichern + mehr davon", - "interestedSavedTitle": "Schon gespeichert — nochmal klicken bestätigt nur", - "notInterested": "Nicht für mich", - "notInterestedTitle": "Weniger davon", - "blockSource": "Quelle ausblenden", - "blockSourceLabel": "{source} ausblenden" - }, - "onboarding": { - "welcome": "Willkommen beim News Hub", - "intro": "In drei Schritten baust du dir deinen persönlichen Newsfeed.", - "stepTopics": "1. Themen", - "stepLanguage": "2. Sprache", - "stepSources": "3. Quellen", - "topicsTitle": "Was interessiert dich?", - "topicsHint": "Wähle mindestens zwei Themen.", - "languageTitle": "In welchen Sprachen liest du?", - "sourcesTitle": "Quellen aus deinen Themen", - "sourcesHint": "Tippe eine Quelle an um sie auszublenden. Du kannst das jederzeit ändern.", - "back": "Zurück", - "next": "Weiter", - "finish": "Fertig", - "finishLoading": "Speichere…" - }, - "reader": { - "back": "Zurück", - "smaller": "Kleiner", - "larger": "Größer", - "save": "Speichern", - "loading": "Lade…", - "notFound": "Artikel nicht gefunden.", - "backToFeed": "Zurück zum Feed", - "openOriginal": "Original öffnen" - }, - "saved": { - "title": "Gespeichert", - "backToFeed": "Feed", - "addUrl": "URL hinzufügen", - "tabUnread": "Ungelesen", - "tabFavorites": "Favoriten", - "tabArchive": "Archiv", - "emptyUnread": "Keine ungelesenen Artikel.", - "emptyUnreadHint": "Reagiere im Feed mit „Interessiert\" um Artikel hier zu sammeln.", - "emptyFavorites": "Noch keine Favoriten.", - "emptyArchive": "Archiv ist leer.", - "badgeOwn": "eigen", - "actionFavorite": "Favorit", - "actionArchive": "Archivieren", - "actionUnarchive": "Wiederherstellen", - "actionDelete": "Löschen", - "actionCategory": "Kategorie", - "categoryNone": "— Keine —" - }, - "categories": { - "all": "Alle", - "manage": "Kategorien verwalten", - "placeholder": "Neue Kategorie…", - "add": "Hinzufügen", - "empty": "Noch keine Kategorien. Erstelle eine oben.", - "rename": "umbenennen", - "delete": "löschen", - "deleteConfirm": "Kategorie löschen? Artikel bleiben erhalten." - }, - "add": { - "title": "Artikel speichern", - "hint": "Füge eine URL ein. Wir extrahieren den Volltext (Mozilla Readability) und legen ihn in deine verschlüsselte Leseliste.", - "backLink": "Gespeichert", - "placeholder": "https://…", - "submit": "Speichern", - "loading": "Lade…", - "errorGeneric": "Speichern fehlgeschlagen" - }, - "preferences": { - "title": "News-Einstellungen", - "page_title_html": "News-Einstellungen — Mana", - "subtitle": "Themen · Sprachen · Gewichtungen", - "backToFeed": "Feed", - "topicsHeading": "Themen", - "topicsHint": "Welche Themen sollen im Feed auftauchen?", - "languagesHeading": "Sprachen", - "sourcesHeading": "Quellen", - "sourcesHint": "Du blockst aktuell {count} Quellen.", - "sourcesHintHtml": "Du blockst aktuell {count} Quellen.", - "sourcesLink": "Quellen verwalten", - "sourcesLinkArrow": "Quellen verwalten →", - "weightsHeading": "Gelernte Gewichtungen", - "weightsHint": "Über Reaktionen lernt der Feed deine Vorlieben: {topics} Themen-Gewichte, {sources} Quellen-Gewichte.", - "weightsReset": "Zurücksetzen", - "weightsResetConfirm": "Alle gelernten Gewichtungen zurücksetzen?", - "onboardingHeading": "Onboarding", - "onboardingHint": "Themen, Sprachen und Quellen neu wählen.", - "onboardingRerun": "Onboarding neu starten" - }, - "sources": { - "title": "Quellen", - "backToPreferences": "Einstellungen", - "hint": "{count} blockiert. Tippe auf eine Quelle um sie ein- oder auszublenden.", - "blocked": "blockiert", - "weightTooltip": "Gewicht: {weight}" - }, - "topics": { - "tech": "Tech", - "wissenschaft": "Wissenschaft", - "weltgeschehen": "Weltgeschehen", - "wirtschaft": "Wirtschaft", - "kultur": "Kultur", - "gesundheit": "Gesundheit", - "politik": "Politik" - }, - "languages": { - "de": "Deutsch", - "en": "English" - }, - "widget": { - "title": "News", - "empty": "Keine ungelesenen News.", - "viewAll": "Alle ansehen" - }, - "workbench": { - "cta_title": "News Hub einrichten", - "cta_hint": "Wähle Themen, Sprachen und Quellen — danach erscheinen hier deine Artikel.", - "cta_action": "Jetzt einrichten", - "err_short": "Fehler", - "empty_short": "Keine neuen Artikel.", - "open_aria": "Öffnen" - } -} diff --git a/apps/mana/apps/web/src/lib/i18n/locales/news/en.json b/apps/mana/apps/web/src/lib/i18n/locales/news/en.json deleted file mode 100644 index 8128b0fee..000000000 --- a/apps/mana/apps/web/src/lib/i18n/locales/news/en.json +++ /dev/null @@ -1,149 +0,0 @@ -{ - "app": { - "name": "News", - "tagline": "Your curated news feed" - }, - "feed": { - "title": "News", - "articles": "{count} articles", - "refresh": "Refresh", - "loading": "Loading articles…", - "empty": "No new articles for your topics.", - "emptyHint": "Try ↻ or pick more topics.", - "loadError": "Loading failed", - "savedLink": "Saved", - "settingsLink": "Settings", - "openArticleAria": "Open article", - "savedBadgeTitle": "In your reading list", - "savedBadgeText": "❤️ saved", - "readingTimeMin": "{n} min" - }, - "reactions": { - "interested": "Interested", - "interestedSaved": "Saved", - "interestedTitle": "Save and see more like this", - "interestedSavedTitle": "Already saved — clicking again only confirms", - "notInterested": "Not for me", - "notInterestedTitle": "Show less of this", - "blockSource": "Hide source", - "blockSourceLabel": "Hide {source}" - }, - "onboarding": { - "welcome": "Welcome to the News Hub", - "intro": "Three steps and you'll have your personal news feed.", - "stepTopics": "1. Topics", - "stepLanguage": "2. Language", - "stepSources": "3. Sources", - "topicsTitle": "What are you interested in?", - "topicsHint": "Pick at least two topics.", - "languageTitle": "Which languages do you read?", - "sourcesTitle": "Sources for your topics", - "sourcesHint": "Tap a source to hide it. You can change this any time.", - "back": "Back", - "next": "Next", - "finish": "Done", - "finishLoading": "Saving…" - }, - "reader": { - "back": "Back", - "smaller": "Smaller", - "larger": "Larger", - "save": "Save", - "loading": "Loading…", - "notFound": "Article not found.", - "backToFeed": "Back to feed", - "openOriginal": "Open original" - }, - "saved": { - "title": "Saved", - "backToFeed": "Feed", - "addUrl": "Add URL", - "tabUnread": "Unread", - "tabFavorites": "Favorites", - "tabArchive": "Archive", - "emptyUnread": "No unread articles.", - "emptyUnreadHint": "Tap \"Interested\" in the feed to collect articles here.", - "emptyFavorites": "No favorites yet.", - "emptyArchive": "Archive is empty.", - "badgeOwn": "own", - "actionFavorite": "Favorite", - "actionArchive": "Archive", - "actionUnarchive": "Restore", - "actionDelete": "Delete", - "actionCategory": "Category", - "categoryNone": "— None —" - }, - "categories": { - "all": "All", - "manage": "Manage categories", - "placeholder": "New category…", - "add": "Add", - "empty": "No categories yet. Create one above.", - "rename": "rename", - "delete": "delete", - "deleteConfirm": "Delete category? Articles are kept." - }, - "add": { - "title": "Save article", - "hint": "Paste a URL. We'll extract the full text (Mozilla Readability) and store it in your encrypted reading list.", - "backLink": "Saved", - "placeholder": "https://…", - "submit": "Save", - "loading": "Loading…", - "errorGeneric": "Save failed" - }, - "preferences": { - "title": "News settings", - "page_title_html": "News settings — Mana", - "subtitle": "Topics · Languages · Weights", - "backToFeed": "Feed", - "topicsHeading": "Topics", - "topicsHint": "Which topics should appear in the feed?", - "languagesHeading": "Languages", - "sourcesHeading": "Sources", - "sourcesHint": "You're currently blocking {count} sources.", - "sourcesHintHtml": "You're currently blocking {count} sources.", - "sourcesLink": "Manage sources", - "sourcesLinkArrow": "Manage sources →", - "weightsHeading": "Learned weights", - "weightsHint": "From your reactions the feed learns your preferences: {topics} topic weights, {sources} source weights.", - "weightsReset": "Reset", - "weightsResetConfirm": "Reset all learned weights?", - "onboardingHeading": "Onboarding", - "onboardingHint": "Pick topics, languages and sources from scratch.", - "onboardingRerun": "Restart onboarding" - }, - "sources": { - "title": "Sources", - "backToPreferences": "Settings", - "hint": "{count} blocked. Tap a source to toggle.", - "blocked": "blocked", - "weightTooltip": "Weight: {weight}" - }, - "topics": { - "tech": "Tech", - "wissenschaft": "Science", - "weltgeschehen": "World", - "wirtschaft": "Business", - "kultur": "Culture", - "gesundheit": "Health", - "politik": "Politics" - }, - "languages": { - "de": "German", - "en": "English" - }, - "widget": { - "title": "News", - "empty": "No unread news.", - "viewAll": "View all" - }, - "workbench": { - "cta_title": "Set up the News Hub", - "cta_hint": "Pick topics, languages and sources — after that your articles appear here.", - "cta_action": "Set up now", - "err_short": "Error", - "empty_short": "No new articles.", - "open_aria": "Open" - } -} diff --git a/apps/mana/apps/web/src/lib/i18n/locales/news/es.json b/apps/mana/apps/web/src/lib/i18n/locales/news/es.json deleted file mode 100644 index 97b53af46..000000000 --- a/apps/mana/apps/web/src/lib/i18n/locales/news/es.json +++ /dev/null @@ -1,149 +0,0 @@ -{ - "app": { - "name": "News", - "tagline": "Tu feed de noticias curado" - }, - "feed": { - "title": "Noticias", - "articles": "{count} artículos", - "refresh": "Recargar", - "loading": "Cargando artículos…", - "empty": "No hay artículos nuevos para tus temas.", - "emptyHint": "Prueba ↻ o añade más temas.", - "loadError": "Error al cargar", - "savedLink": "Guardados", - "settingsLink": "Ajustes", - "openArticleAria": "Abrir artículo", - "savedBadgeTitle": "En tu lista de lectura", - "savedBadgeText": "❤️ guardado", - "readingTimeMin": "{n} min" - }, - "reactions": { - "interested": "Me interesa", - "interestedSaved": "Guardado", - "interestedTitle": "Guardar y ver más como esto", - "interestedSavedTitle": "Ya guardado — volver a hacer clic solo confirma", - "notInterested": "No es para mí", - "notInterestedTitle": "Mostrar menos de esto", - "blockSource": "Ocultar fuente", - "blockSourceLabel": "Ocultar {source}" - }, - "onboarding": { - "welcome": "Bienvenido al News Hub", - "intro": "En tres pasos crearás tu feed personal.", - "stepTopics": "1. Temas", - "stepLanguage": "2. Idioma", - "stepSources": "3. Fuentes", - "topicsTitle": "¿Qué te interesa?", - "topicsHint": "Elige al menos dos temas.", - "languageTitle": "¿En qué idiomas lees?", - "sourcesTitle": "Fuentes de tus temas", - "sourcesHint": "Toca una fuente para ocultarla. Puedes cambiarlo cuando quieras.", - "back": "Atrás", - "next": "Siguiente", - "finish": "Listo", - "finishLoading": "Guardando…" - }, - "reader": { - "back": "Atrás", - "smaller": "Menor", - "larger": "Mayor", - "save": "Guardar", - "loading": "Cargando…", - "notFound": "Artículo no encontrado.", - "backToFeed": "Volver al feed", - "openOriginal": "Abrir original" - }, - "saved": { - "title": "Guardados", - "backToFeed": "Feed", - "addUrl": "Añadir URL", - "tabUnread": "No leídos", - "tabFavorites": "Favoritos", - "tabArchive": "Archivo", - "emptyUnread": "No hay artículos sin leer.", - "emptyUnreadHint": "Toca \"Me interesa\" en el feed para coleccionarlos aquí.", - "emptyFavorites": "Aún no hay favoritos.", - "emptyArchive": "El archivo está vacío.", - "badgeOwn": "propio", - "actionFavorite": "Favorito", - "actionArchive": "Archivar", - "actionUnarchive": "Restaurar", - "actionDelete": "Eliminar", - "actionCategory": "Categoría", - "categoryNone": "— Ninguna —" - }, - "categories": { - "all": "Todos", - "manage": "Gestionar categorías", - "placeholder": "Nueva categoría…", - "add": "Añadir", - "empty": "Aún no hay categorías. Crea una arriba.", - "rename": "renombrar", - "delete": "eliminar", - "deleteConfirm": "¿Eliminar la categoría? Los artículos se mantienen." - }, - "add": { - "title": "Guardar artículo", - "hint": "Pega una URL. Extraemos el texto completo (Mozilla Readability) y lo guardamos en tu lista de lectura cifrada.", - "backLink": "Guardados", - "placeholder": "https://…", - "submit": "Guardar", - "loading": "Cargando…", - "errorGeneric": "Error al guardar" - }, - "preferences": { - "title": "Ajustes de noticias", - "page_title_html": "Ajustes de noticias — Mana", - "subtitle": "Temas · Idiomas · Pesos", - "backToFeed": "Feed", - "topicsHeading": "Temas", - "topicsHint": "¿Qué temas deben aparecer en el feed?", - "languagesHeading": "Idiomas", - "sourcesHeading": "Fuentes", - "sourcesHint": "Estás bloqueando {count} fuentes.", - "sourcesHintHtml": "Estás bloqueando {count} fuentes.", - "sourcesLink": "Gestionar fuentes", - "sourcesLinkArrow": "Gestionar fuentes →", - "weightsHeading": "Pesos aprendidos", - "weightsHint": "El feed aprende tus preferencias a partir de tus reacciones: {topics} pesos de temas, {sources} pesos de fuentes.", - "weightsReset": "Restablecer", - "weightsResetConfirm": "¿Restablecer todos los pesos aprendidos?", - "onboardingHeading": "Onboarding", - "onboardingHint": "Vuelve a elegir temas, idiomas y fuentes.", - "onboardingRerun": "Reiniciar onboarding" - }, - "sources": { - "title": "Fuentes", - "backToPreferences": "Ajustes", - "hint": "{count} bloqueadas. Toca una fuente para alternar.", - "blocked": "bloqueada", - "weightTooltip": "Peso: {weight}" - }, - "topics": { - "tech": "Tecnología", - "wissenschaft": "Ciencia", - "weltgeschehen": "Mundo", - "wirtschaft": "Economía", - "kultur": "Cultura", - "gesundheit": "Salud", - "politik": "Política" - }, - "languages": { - "de": "Alemán", - "en": "Inglés" - }, - "widget": { - "title": "Noticias", - "empty": "Sin noticias por leer.", - "viewAll": "Ver todo" - }, - "workbench": { - "cta_title": "Configurar el News Hub", - "cta_hint": "Elige temas, idiomas y fuentes — luego tus artículos aparecerán aquí.", - "cta_action": "Configurar ahora", - "err_short": "Error", - "empty_short": "No hay artículos nuevos.", - "open_aria": "Abrir" - } -} diff --git a/apps/mana/apps/web/src/lib/i18n/locales/news/fr.json b/apps/mana/apps/web/src/lib/i18n/locales/news/fr.json deleted file mode 100644 index c569fc77e..000000000 --- a/apps/mana/apps/web/src/lib/i18n/locales/news/fr.json +++ /dev/null @@ -1,149 +0,0 @@ -{ - "app": { - "name": "News", - "tagline": "Ton fil d'actualité personnalisé" - }, - "feed": { - "title": "Actualités", - "articles": "{count} articles", - "refresh": "Recharger", - "loading": "Chargement des articles…", - "empty": "Aucun nouvel article pour tes thèmes.", - "emptyHint": "Essaie ↻ ou ajoute des thèmes.", - "loadError": "Échec du chargement", - "savedLink": "Enregistrés", - "settingsLink": "Réglages", - "openArticleAria": "Ouvrir l'article", - "savedBadgeTitle": "Dans ta liste de lecture", - "savedBadgeText": "❤️ enregistré", - "readingTimeMin": "{n} min" - }, - "reactions": { - "interested": "Intéressant", - "interestedSaved": "Enregistré", - "interestedTitle": "Enregistrer et en voir plus", - "interestedSavedTitle": "Déjà enregistré — recliquer ne fait que confirmer", - "notInterested": "Pas pour moi", - "notInterestedTitle": "Moins de ce genre", - "blockSource": "Masquer la source", - "blockSourceLabel": "Masquer {source}" - }, - "onboarding": { - "welcome": "Bienvenue dans le News Hub", - "intro": "Trois étapes pour bâtir ton fil personnel.", - "stepTopics": "1. Thèmes", - "stepLanguage": "2. Langue", - "stepSources": "3. Sources", - "topicsTitle": "Qu'est-ce qui t'intéresse ?", - "topicsHint": "Choisis au moins deux thèmes.", - "languageTitle": "Dans quelles langues lis-tu ?", - "sourcesTitle": "Sources de tes thèmes", - "sourcesHint": "Touche une source pour la masquer. Tu peux changer à tout moment.", - "back": "Retour", - "next": "Suivant", - "finish": "Terminé", - "finishLoading": "Enregistrement…" - }, - "reader": { - "back": "Retour", - "smaller": "Plus petit", - "larger": "Plus grand", - "save": "Enregistrer", - "loading": "Chargement…", - "notFound": "Article introuvable.", - "backToFeed": "Retour au fil", - "openOriginal": "Ouvrir l'original" - }, - "saved": { - "title": "Enregistrés", - "backToFeed": "Fil", - "addUrl": "Ajouter une URL", - "tabUnread": "Non lus", - "tabFavorites": "Favoris", - "tabArchive": "Archives", - "emptyUnread": "Aucun article non lu.", - "emptyUnreadHint": "Touche « Intéressant » dans le fil pour collecter les articles ici.", - "emptyFavorites": "Aucun favori pour l'instant.", - "emptyArchive": "Les archives sont vides.", - "badgeOwn": "perso", - "actionFavorite": "Favori", - "actionArchive": "Archiver", - "actionUnarchive": "Restaurer", - "actionDelete": "Supprimer", - "actionCategory": "Catégorie", - "categoryNone": "— Aucune —" - }, - "categories": { - "all": "Tous", - "manage": "Gérer les catégories", - "placeholder": "Nouvelle catégorie…", - "add": "Ajouter", - "empty": "Aucune catégorie. Crées-en une au-dessus.", - "rename": "renommer", - "delete": "supprimer", - "deleteConfirm": "Supprimer la catégorie ? Les articles sont conservés." - }, - "add": { - "title": "Enregistrer un article", - "hint": "Colle une URL. Nous extrayons le texte complet (Mozilla Readability) et l'ajoutons à ta liste de lecture chiffrée.", - "backLink": "Enregistrés", - "placeholder": "https://…", - "submit": "Enregistrer", - "loading": "Chargement…", - "errorGeneric": "Échec de l'enregistrement" - }, - "preferences": { - "title": "Réglages des actualités", - "page_title_html": "Réglages des actualités — Mana", - "subtitle": "Thèmes · Langues · Pondérations", - "backToFeed": "Fil", - "topicsHeading": "Thèmes", - "topicsHint": "Quels thèmes doivent apparaître dans le fil ?", - "languagesHeading": "Langues", - "sourcesHeading": "Sources", - "sourcesHint": "Tu bloques actuellement {count} sources.", - "sourcesHintHtml": "Tu bloques actuellement {count} sources.", - "sourcesLink": "Gérer les sources", - "sourcesLinkArrow": "Gérer les sources →", - "weightsHeading": "Pondérations apprises", - "weightsHint": "Le fil apprend tes préférences via tes réactions : {topics} pondérations de thèmes, {sources} pondérations de sources.", - "weightsReset": "Réinitialiser", - "weightsResetConfirm": "Réinitialiser toutes les pondérations apprises ?", - "onboardingHeading": "Onboarding", - "onboardingHint": "Re-choisis thèmes, langues et sources.", - "onboardingRerun": "Recommencer l'onboarding" - }, - "sources": { - "title": "Sources", - "backToPreferences": "Réglages", - "hint": "{count} bloquées. Touche une source pour basculer.", - "blocked": "bloquée", - "weightTooltip": "Poids : {weight}" - }, - "topics": { - "tech": "Tech", - "wissenschaft": "Sciences", - "weltgeschehen": "Monde", - "wirtschaft": "Économie", - "kultur": "Culture", - "gesundheit": "Santé", - "politik": "Politique" - }, - "languages": { - "de": "Allemand", - "en": "Anglais" - }, - "widget": { - "title": "Actualités", - "empty": "Aucune actualité non lue.", - "viewAll": "Tout voir" - }, - "workbench": { - "cta_title": "Configurer le News Hub", - "cta_hint": "Choisis thèmes, langues et sources — ensuite tes articles apparaîtront ici.", - "cta_action": "Configurer maintenant", - "err_short": "Erreur", - "empty_short": "Aucun nouvel article.", - "open_aria": "Ouvrir" - } -} diff --git a/apps/mana/apps/web/src/lib/i18n/locales/news/it.json b/apps/mana/apps/web/src/lib/i18n/locales/news/it.json deleted file mode 100644 index d0c5af8b3..000000000 --- a/apps/mana/apps/web/src/lib/i18n/locales/news/it.json +++ /dev/null @@ -1,149 +0,0 @@ -{ - "app": { - "name": "News", - "tagline": "Il tuo feed di notizie curato" - }, - "feed": { - "title": "Notizie", - "articles": "{count} articoli", - "refresh": "Ricarica", - "loading": "Caricamento articoli…", - "empty": "Nessun nuovo articolo per i tuoi temi.", - "emptyHint": "Prova ↻ o aggiungi temi.", - "loadError": "Errore di caricamento", - "savedLink": "Salvati", - "settingsLink": "Impostazioni", - "openArticleAria": "Apri articolo", - "savedBadgeTitle": "Nella tua lista di lettura", - "savedBadgeText": "❤️ salvato", - "readingTimeMin": "{n} min" - }, - "reactions": { - "interested": "Mi interessa", - "interestedSaved": "Salvato", - "interestedTitle": "Salva e mostra di più di questo", - "interestedSavedTitle": "Già salvato — un altro click conferma soltanto", - "notInterested": "Non per me", - "notInterestedTitle": "Mostra di meno", - "blockSource": "Nascondi fonte", - "blockSourceLabel": "Nascondi {source}" - }, - "onboarding": { - "welcome": "Benvenuto nel News Hub", - "intro": "In tre passi crei il tuo feed personale.", - "stepTopics": "1. Temi", - "stepLanguage": "2. Lingua", - "stepSources": "3. Fonti", - "topicsTitle": "Cosa ti interessa?", - "topicsHint": "Scegli almeno due temi.", - "languageTitle": "In quali lingue leggi?", - "sourcesTitle": "Fonti dei tuoi temi", - "sourcesHint": "Tocca una fonte per nasconderla. Puoi cambiare in qualsiasi momento.", - "back": "Indietro", - "next": "Avanti", - "finish": "Fatto", - "finishLoading": "Salvataggio…" - }, - "reader": { - "back": "Indietro", - "smaller": "Più piccolo", - "larger": "Più grande", - "save": "Salva", - "loading": "Caricamento…", - "notFound": "Articolo non trovato.", - "backToFeed": "Torna al feed", - "openOriginal": "Apri originale" - }, - "saved": { - "title": "Salvati", - "backToFeed": "Feed", - "addUrl": "Aggiungi URL", - "tabUnread": "Da leggere", - "tabFavorites": "Preferiti", - "tabArchive": "Archivio", - "emptyUnread": "Nessun articolo da leggere.", - "emptyUnreadHint": "Tocca \"Mi interessa\" nel feed per raccogliere articoli qui.", - "emptyFavorites": "Ancora nessun preferito.", - "emptyArchive": "L'archivio è vuoto.", - "badgeOwn": "personale", - "actionFavorite": "Preferito", - "actionArchive": "Archivia", - "actionUnarchive": "Ripristina", - "actionDelete": "Elimina", - "actionCategory": "Categoria", - "categoryNone": "— Nessuna —" - }, - "categories": { - "all": "Tutti", - "manage": "Gestisci categorie", - "placeholder": "Nuova categoria…", - "add": "Aggiungi", - "empty": "Nessuna categoria. Creane una sopra.", - "rename": "rinomina", - "delete": "elimina", - "deleteConfirm": "Eliminare la categoria? Gli articoli vengono mantenuti." - }, - "add": { - "title": "Salva articolo", - "hint": "Incolla un URL. Estraiamo il testo completo (Mozilla Readability) e lo salviamo nella tua lista di lettura cifrata.", - "backLink": "Salvati", - "placeholder": "https://…", - "submit": "Salva", - "loading": "Caricamento…", - "errorGeneric": "Salvataggio fallito" - }, - "preferences": { - "title": "Impostazioni notizie", - "page_title_html": "Impostazioni notizie — Mana", - "subtitle": "Temi · Lingue · Pesi", - "backToFeed": "Feed", - "topicsHeading": "Temi", - "topicsHint": "Quali temi devono apparire nel feed?", - "languagesHeading": "Lingue", - "sourcesHeading": "Fonti", - "sourcesHint": "Stai bloccando {count} fonti.", - "sourcesHintHtml": "Stai bloccando {count} fonti.", - "sourcesLink": "Gestisci fonti", - "sourcesLinkArrow": "Gestisci fonti →", - "weightsHeading": "Pesi appresi", - "weightsHint": "Dalle tue reazioni il feed impara le tue preferenze: {topics} pesi tema, {sources} pesi fonte.", - "weightsReset": "Reimposta", - "weightsResetConfirm": "Reimpostare tutti i pesi appresi?", - "onboardingHeading": "Onboarding", - "onboardingHint": "Riscegli temi, lingue e fonti.", - "onboardingRerun": "Riavvia onboarding" - }, - "sources": { - "title": "Fonti", - "backToPreferences": "Impostazioni", - "hint": "{count} bloccate. Tocca una fonte per cambiare stato.", - "blocked": "bloccata", - "weightTooltip": "Peso: {weight}" - }, - "topics": { - "tech": "Tech", - "wissenschaft": "Scienza", - "weltgeschehen": "Mondo", - "wirtschaft": "Economia", - "kultur": "Cultura", - "gesundheit": "Salute", - "politik": "Politica" - }, - "languages": { - "de": "Tedesco", - "en": "Inglese" - }, - "widget": { - "title": "Notizie", - "empty": "Nessuna notizia da leggere.", - "viewAll": "Vedi tutte" - }, - "workbench": { - "cta_title": "Configura il News Hub", - "cta_hint": "Scegli temi, lingue e fonti — poi i tuoi articoli appariranno qui.", - "cta_action": "Configura ora", - "err_short": "Errore", - "empty_short": "Nessun nuovo articolo.", - "open_aria": "Apri" - } -} diff --git a/apps/mana/apps/web/src/lib/modules/articles/migrations/from-news.ts b/apps/mana/apps/web/src/lib/modules/articles/migrations/from-news.ts deleted file mode 100644 index 4fdf832cf..000000000 --- a/apps/mana/apps/web/src/lib/modules/articles/migrations/from-news.ts +++ /dev/null @@ -1,164 +0,0 @@ -/** - * One-off migration: move `newsArticles` with `type='saved'` into the - * new `articles` module. - * - * Runs at app-shell boot (from routes/(app)/+layout.svelte) rather than - * inside the Dexie `.upgrade()` hook because we need the encryption - * layer initialised: the source rows are encrypted under the - * `newsArticles` field allowlist, the target rows need to be - * re-encrypted under the `articles` allowlist, and both roundtrips - * require Web Crypto + the master key — which the Dexie upgrade path - * runs before. - * - * Idempotent: a localStorage sentinel prevents re-runs per device. - * The original rows are soft-deleted (deletedAt stamped) so the sync - * layer propagates the removal to the server and to other devices. - * - * Migration mapping: - * newsArticles.isArchived = true → articles.status = 'archived' - * newsArticles.isRead = true → articles.status = 'finished' - * otherwise → articles.status = 'unread' - * - * isFavorite, createdAt, userId carry across. `sourceSlug` / - * `sourceCuratedId` / `categoryId` don't have a counterpart on - * articles (they're news-feed-specific) and are dropped — the user's - * reading-list view doesn't depend on them. - */ - -import { db } from '$lib/data/database'; -import { encryptRecord, decryptRecords } from '$lib/data/crypto'; -import { hasAnyEncryption } from '$lib/data/crypto/registry'; -import type { LocalArticle as NewLocalArticle, ArticleStatus } from '../types'; - -const SENTINEL_KEY = 'mana:articles:from-news-migration:v1'; - -// Shape of the source rows we care about. Kept narrow so the migration -// stays decoupled from the news module's evolving type file. -interface LegacyNewsArticle { - id: string; - type: 'curated' | 'saved'; - originalUrl: string; - title: string; - excerpt: string | null; - content: string; - htmlContent: string | null; - author: string | null; - siteName: string | null; - imageUrl: string | null; - wordCount: number | null; - readingTimeMinutes: number | null; - publishedAt: string | null; - isArchived?: boolean; - isRead?: boolean; - isFavorite?: boolean; - userId?: string; - createdAt?: string; - updatedAt?: string; - deletedAt?: string | null; -} - -function statusFor(row: LegacyNewsArticle): ArticleStatus { - if (row.isArchived) return 'archived'; - if (row.isRead) return 'finished'; - return 'unread'; -} - -/** - * Run the migration once per device. Returns the number of rows moved. - * Fire-and-forget from app boot; errors are logged but never thrown so - * a single broken row never blocks the rest of the app from starting. - */ -export async function runArticlesFromNewsMigration(): Promise { - if (typeof window === 'undefined') return 0; - if (window.localStorage.getItem(SENTINEL_KEY)) return 0; - - // The migration requires the crypto layer to be live. If the app is - // running entirely plaintext (Phase 1 bootstrap or a test harness), - // decryptRecords is a pass-through so this still works — we check - // anyway as a defensive gate and bail if the registry isn't ready. - try { - // Access the flag so linters don't flag the import as unused when - // someone later decides the gate isn't worth keeping. The call is - // cheap either way. - hasAnyEncryption(); - } catch { - return 0; - } - - try { - const newsTable = db.table('newsArticles'); - const articlesTable = db.table('articles'); - - const candidates = await newsTable.where('type').equals('saved').toArray(); - const visible = candidates.filter((row) => !row.deletedAt); - if (visible.length === 0) { - window.localStorage.setItem(SENTINEL_KEY, new Date().toISOString()); - return 0; - } - - const decrypted = (await decryptRecords( - 'newsArticles', - visible as unknown as Record[] - )) as unknown as LegacyNewsArticle[]; - - const now = new Date().toISOString(); - let moved = 0; - - // Separate transactions: one write batch per row with its own - // encryption roundtrip, so a single bad row doesn't lose the - // batch. Dexie auto-batches the internal index updates either way. - for (const row of decrypted) { - try { - const newRow: NewLocalArticle = { - id: crypto.randomUUID(), - originalUrl: row.originalUrl, - title: row.title, - excerpt: row.excerpt, - content: row.content, - htmlContent: row.htmlContent, - author: row.author, - siteName: row.siteName, - imageUrl: row.imageUrl, - wordCount: row.wordCount, - readingTimeMinutes: row.readingTimeMinutes, - publishedAt: row.publishedAt, - status: statusFor(row), - readingProgress: 0, - isFavorite: row.isFavorite ?? false, - savedAt: row.createdAt ?? now, - readAt: row.isRead ? (row.updatedAt ?? now) : null, - userNote: null, - extractedVersion: 1, - // userId is stamped by the Dexie creating-hook from the active - // session — don't set it manually, let the hook do its job. - }; - await encryptRecord('articles', newRow); - await articlesTable.add(newRow); - // Soft-delete the source so the sync engine removes it from - // the server + other devices. Keep it in the local table so - // if someone later rolls back the migration they can still - // see what was there. - await newsTable.update(row.id, { deletedAt: now }); - moved++; - } catch (rowErr) { - console.warn(`[articles/from-news] skipping row ${row.id} — ${(rowErr as Error).message}`); - } - } - - window.localStorage.setItem(SENTINEL_KEY, now); - if (moved > 0) { - console.info(`[articles/from-news] migrated ${moved} saved article(s) into /articles`); - } - return moved; - } catch (err) { - console.error('[articles/from-news] migration failed:', err); - return 0; - } -} - -/** Clear the sentinel so the next boot re-runs. Test / recovery helper only. */ -export function resetArticlesFromNewsSentinel(): void { - if (typeof window !== 'undefined') { - window.localStorage.removeItem(SENTINEL_KEY); - } -} diff --git a/apps/mana/apps/web/src/lib/modules/news-research/ListView.svelte b/apps/mana/apps/web/src/lib/modules/news-research/ListView.svelte index 13a646998..bf9f146d4 100644 --- a/apps/mana/apps/web/src/lib/modules/news-research/ListView.svelte +++ b/apps/mana/apps/web/src/lib/modules/news-research/ListView.svelte @@ -14,14 +14,10 @@ import type { ViewProps } from '$lib/app-registry'; import { researchSessionStore } from '$lib/modules/news-research/stores/session.svelte'; import { articlesStore } from '$lib/modules/articles/stores/articles.svelte'; - import { preferencesStore } from '$lib/modules/news/stores/preferences.svelte'; - import { usePreferences } from '$lib/modules/news/queries'; const {}: ViewProps = $props(); const store = researchSessionStore; - const prefs$ = usePreferences(); - const pinnedUrls = $derived(new Set((prefs$.value?.customFeeds ?? []).map((f) => f.url))); let mode = $state<'query' | 'site'>('query'); let query = $state(''); @@ -58,15 +54,6 @@ feedsOpen = false; } - async function togglePin(feed: { url: string; title: string | null }) { - if (pinnedUrls.has(feed.url)) { - const existing = (prefs$.value?.customFeeds ?? []).find((f) => f.url === feed.url); - if (existing) await preferencesStore.unpinCustomFeed(existing.id); - } else { - await preferencesStore.pinCustomFeed({ url: feed.url, title: feed.title ?? feed.url }); - } - } - async function onSave(articleUrl: string) { savingUrl = articleUrl; saveError = null; @@ -157,15 +144,6 @@ /> {feed.title ?? feed.url} - {/each} @@ -356,17 +334,6 @@ text-overflow: ellipsis; white-space: nowrap; } - .pin { - background: transparent; - border: none; - cursor: pointer; - font-size: 0.95rem; - color: hsl(var(--color-muted-foreground)); - padding: 0 0.25rem; - } - .pin.pinned { - color: hsl(var(--color-primary)); - } .results-head { display: flex; justify-content: space-between; diff --git a/apps/mana/apps/web/src/lib/modules/news/ListView.svelte b/apps/mana/apps/web/src/lib/modules/news/ListView.svelte deleted file mode 100644 index 5567eef49..000000000 --- a/apps/mana/apps/web/src/lib/modules/news/ListView.svelte +++ /dev/null @@ -1,369 +0,0 @@ - - - -
- {#if !prefs.onboardingCompleted} -
-

{$_('news.workbench.cta_title')}

-

- {$_('news.workbench.cta_hint')} -

- {$_('news.workbench.cta_action')} -
- {:else} -
-
- {$_('news.feed.articles', { values: { count: ranked.length } })} - {#if feedCacheStore.lastError} - · {$_('news.workbench.err_short')} - {/if} -
-
- - 📑 - -
-
- - {#if ranked.length === 0} -
- {#if pool.length === 0} -

{$_('news.feed.loading')}

- {:else} -

{$_('news.workbench.empty_short')}

- - {/if} -
- {:else} -
    - {#each ranked.slice(0, 30) as { article } (article.id)} -
  • - {#if article.imageUrl} - - {/if} -
    -
    - {article.siteName} - · - {formatRelativeTime(article.publishedAt)} -
    - -
    - - - -
    -
    -
  • - {/each} -
- {/if} - {/if} -
- - diff --git a/apps/mana/apps/web/src/lib/modules/news/api.ts b/apps/mana/apps/web/src/lib/modules/news/api.ts deleted file mode 100644 index 3af3cd377..000000000 --- a/apps/mana/apps/web/src/lib/modules/news/api.ts +++ /dev/null @@ -1,89 +0,0 @@ -/** - * News API client — talks to apps/api `/api/v1/news/*`. - * - * Two flavors of endpoints: - * - GET /feed — pulls the curated pool, with topic/lang filters - * - POST /extract/* — Mozilla Readability for ad-hoc URL saves - * - * The base URL comes from `getManaApiUrl()`, which on the client reads the - * browser-injected `__PUBLIC_MANA_API_URL__` (set from - * `PUBLIC_MANA_API_URL_CLIENT` in hooks.server.ts → e.g. - * `https://mana-api.mana.how`) and on the server reads `process.env` - * directly. Reading `$env/dynamic/public.PUBLIC_MANA_API_URL` here would - * leak the SSR-side internal Docker hostname (`http://mana-api:3060`) to - * the browser and trip CSP / DNS. - * - * Auth is the unified Mana JWT pulled from `authStore.getAccessToken()` - * and attached as a `Authorization: Bearer …` header. The credentials/ - * cookie path does NOT work — the apps/api authMiddleware only reads - * the Authorization header. Initially we passed `credentials: 'include'` - * thinking the cookie alone was enough, which made every browser-side - * fetch return 401 because mana-api never sees a token. - */ - -import { authStore } from '$lib/stores/auth.svelte'; -import { getManaApiUrl } from '$lib/api/config'; - -async function authHeader(): Promise> { - // getValidToken (not getAccessToken) — runs the token through the - // tokenManager so it refreshes if expired. getAccessToken just reads - // localStorage and returns null/stale, which is what made the first - // pass at this fix still 401. sync.ts uses the same getValidToken. - const token = await authStore.getValidToken(); - return token ? { Authorization: `Bearer ${token}` } : {}; -} - -export interface FeedArticleDto { - id: string; - originalUrl: string; - title: string; - excerpt: string | null; - content: string; - htmlContent: string | null; - author: string | null; - siteName: string; - sourceSlug: string; - imageUrl: string | null; - topic: string; - language: string; - wordCount: number | null; - readingTimeMinutes: number | null; - publishedAt: string | null; - ingestedAt: string; -} - -export interface FeedQuery { - topics?: string[]; - lang?: 'de' | 'en' | 'all'; - since?: string; - limit?: number; - offset?: number; -} - -export async function fetchFeed( - query: FeedQuery = {}, - fetchImpl: typeof fetch = fetch -): Promise { - const params = new URLSearchParams(); - if (query.topics && query.topics.length > 0) { - params.set('topics', query.topics.join(',')); - } - if (query.lang && query.lang !== 'all') params.set('lang', query.lang); - if (query.since) params.set('since', query.since); - if (query.limit != null) params.set('limit', String(query.limit)); - if (query.offset != null) params.set('offset', String(query.offset)); - - const url = `${getManaApiUrl()}/api/v1/news/feed${params.toString() ? `?${params}` : ''}`; - const response = await fetchImpl(url, { - headers: await authHeader(), - }); - if (!response.ok) { - throw new Error(`fetchFeed failed: ${response.status}`); - } - return (await response.json()) as FeedArticleDto[]; -} - -// 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/collections.ts b/apps/mana/apps/web/src/lib/modules/news/collections.ts deleted file mode 100644 index 6dbbba4b0..000000000 --- a/apps/mana/apps/web/src/lib/modules/news/collections.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * News module — Dexie table accessors and seed data. - */ - -import { db } from '$lib/data/database'; -import type { - LocalArticle, - LocalCachedArticle, - LocalCategory, - LocalPreferences, - LocalReaction, -} from './types'; -import { PREFERENCES_ID } from './types'; - -export const articleTable = db.table('newsArticles'); -export const categoryTable = db.table('newsCategories'); -export const preferencesTable = db.table('newsPreferences'); -export const reactionTable = db.table('newsReactions'); -export const cachedFeedTable = db.table('newsCachedFeed'); - -/** - * Default preferences row written on first launch (before the user runs - * the onboarding flow). `onboardingCompleted: false` is what triggers - * the onboarding view to render instead of the feed. - */ -export const DEFAULT_PREFERENCES: LocalPreferences = { - id: PREFERENCES_ID, - selectedTopics: [], - blockedSources: [], - preferredLanguages: ['de', 'en'], - topicWeights: {}, - sourceWeights: {}, - onboardingCompleted: false, - customFeeds: [], -}; diff --git a/apps/mana/apps/web/src/lib/modules/news/feed-engine.ts b/apps/mana/apps/web/src/lib/modules/news/feed-engine.ts deleted file mode 100644 index c24bdf46f..000000000 --- a/apps/mana/apps/web/src/lib/modules/news/feed-engine.ts +++ /dev/null @@ -1,204 +0,0 @@ -/** - * Pure feed-engine: takes the raw cached pool + the user's preferences - * and reactions, returns a sorted, filtered list of articles to show. - * - * No state, no I/O — every input is passed in. The store layer wires - * this up against live Dexie data via $derived. - * - * Scoring formula (deterministic, no ML): - * score = recency × topicWeight × sourceWeight - * - * recency 1.0 for <1h old, decays linearly to 0 over 7 days - * topicWeight default 1.0, +0.1 per "interested" reaction in that - * topic, −0.05 per "not_interested" (clamped 0.1..3.0) - * sourceWeight same dynamics keyed on source slug - * - * Hard filters (applied before scoring): - * - article topic must be in preferences.selectedTopics - * - article source must NOT be in preferences.blockedSources - * - language must be in preferences.preferredLanguages - * - article must not have a prior reaction - * (interested → moved to reading list, not_interested/hidden → - * explicitly suppressed) - */ - -import type { LocalCachedArticle, Preferences, Reaction, ReactionKind } from './types'; - -export const TOPIC_WEIGHT_DEFAULT = 1.0; -export const TOPIC_WEIGHT_MIN = 0.1; -export const TOPIC_WEIGHT_MAX = 3.0; - -export const INTERESTED_DELTA = 0.1; -export const NOT_INTERESTED_DELTA = -0.05; - -const RECENCY_WINDOW_HOURS = 168; // 7 days - -function recencyScore(publishedAt: string | null): number { - if (!publishedAt) return 0.1; - const ageH = (Date.now() - new Date(publishedAt).getTime()) / 3.6e6; - if (ageH < 0) return 1.0; - return Math.max(0, 1 - ageH / RECENCY_WINDOW_HOURS); -} - -export interface ScoreContext { - prefs: Preferences; - /** - * Set of curatedArticleIds the user has actively dismissed - * (`not_interested`, `hidden`, or via `source_blocked`). Used as a - * hard hide-from-feed filter. - * - * `interested` reactions are NOT in this set on purpose — those - * articles stay visible in the feed (with a saved-badge) so the - * user can keep reading and clicking around without articles - * disappearing the moment they tap "❤️". The reading list remains - * the source of truth for "what did I save". - */ - dismissedIds: ReadonlySet; - /** Set of curatedArticleIds the user marked as interested. */ - interestedIds: ReadonlySet; -} - -/** - * Split the user's reactions into two sets: dismissed (hard-hide - * from feed) and interested (keep visible, badge in UI). Built once - * per render and reused across all scoreArticle calls. - * - * `source_blocked` reactions are NOT added to dismissedIds even - * though they hide articles — the source-level filter in - * `scoreArticle` handles those via `prefs.blockedSources` instead, - * so adding them here would be a no-op duplicate. - */ -export function buildReactionSets(reactions: readonly Reaction[]): { - dismissedIds: Set; - interestedIds: Set; -} { - const dismissedIds = new Set(); - const interestedIds = new Set(); - for (const r of reactions) { - if (r.reaction === 'interested') { - interestedIds.add(r.articleId); - } else if (r.reaction === 'not_interested' || r.reaction === 'hidden') { - dismissedIds.add(r.articleId); - } - } - return { dismissedIds, interestedIds }; -} - -/** - * @deprecated Kept for backwards compat with the dashboard widget. - * New call sites should use `buildReactionSets` and the - * dismissedIds/interestedIds shape instead. This helper now returns - * only the dismissed ids — same effect on the feed filter, but it - * means widgets that haven't migrated still hide - * `not_interested`/`hidden` articles correctly while leaving - * `interested` articles visible (matching the new feed behavior). - */ -export function buildReactedIds(reactions: readonly Reaction[]): Set { - return buildReactionSets(reactions).dismissedIds; -} - -/** - * Returns a number ≥ 0 if the article passes filters, or `null` if it - * should be hidden entirely. Callers sort by descending score. - */ -export function scoreArticle(article: LocalCachedArticle, ctx: ScoreContext): number | null { - const { prefs, dismissedIds } = ctx; - - if (prefs.selectedTopics.length > 0 && !prefs.selectedTopics.includes(article.topic as never)) { - return null; - } - if (prefs.blockedSources.includes(article.sourceSlug)) return null; - if ( - prefs.preferredLanguages.length > 0 && - !prefs.preferredLanguages.includes(article.language as never) - ) { - return null; - } - // Only ACTIVELY DISMISSED articles are filtered out. `interested` - // reactions stay visible in the feed (the user's read state is - // communicated via the badge in the UI, not by hiding the card). - if (dismissedIds.has(article.id)) return null; - - const topicW = prefs.topicWeights[article.topic] ?? TOPIC_WEIGHT_DEFAULT; - const sourceW = prefs.sourceWeights[article.sourceSlug] ?? TOPIC_WEIGHT_DEFAULT; - const recency = recencyScore(article.publishedAt); - - // Floor recency at 0.05 so very old but highly-weighted sources still - // surface above brand-new but unweighted ones — keeps the feed from - // devolving into a pure recency stream. - const floored = Math.max(recency, 0.05); - return floored * topicW * sourceW; -} - -export interface ScoredArticle { - article: LocalCachedArticle; - score: number; -} - -/** - * Score the whole pool, drop the rejected ones, and return descending - * by score. Stable: ties broken by `publishedAt` desc. - */ -export function rankFeed(pool: readonly LocalCachedArticle[], ctx: ScoreContext): ScoredArticle[] { - const out: ScoredArticle[] = []; - for (const article of pool) { - const score = scoreArticle(article, ctx); - if (score == null) continue; - out.push({ article, score }); - } - out.sort((a, b) => { - if (b.score !== a.score) return b.score - a.score; - const ap = a.article.publishedAt ?? ''; - const bp = b.article.publishedAt ?? ''; - return bp.localeCompare(ap); - }); - return out; -} - -// ─── Weight updates (returned as a partial Preferences diff) ─── - -export interface WeightDiff { - topicWeights?: Record; - sourceWeights?: Record; - blockedSources?: string[]; -} - -function clamp(n: number): number { - return Math.max(TOPIC_WEIGHT_MIN, Math.min(TOPIC_WEIGHT_MAX, n)); -} - -/** - * Compute the preferences delta for a new reaction. The store layer - * merges this back onto the existing preferences row in a single - * `update()` call. - */ -export function applyReaction( - prefs: Preferences, - reaction: ReactionKind, - topic: string, - sourceSlug: string -): WeightDiff { - if (reaction === 'source_blocked') { - if (prefs.blockedSources.includes(sourceSlug)) return {}; - return { blockedSources: [...prefs.blockedSources, sourceSlug] }; - } - - if (reaction === 'hidden') { - // "Hidden" is a per-article suppression — no weight change. The - // reaction row alone is enough for `reactedIds` to filter it. - return {}; - } - - const delta = reaction === 'interested' ? INTERESTED_DELTA : NOT_INTERESTED_DELTA; - - const currentTopic = prefs.topicWeights[topic] ?? TOPIC_WEIGHT_DEFAULT; - const currentSource = prefs.sourceWeights[sourceSlug] ?? TOPIC_WEIGHT_DEFAULT; - - return { - topicWeights: { ...prefs.topicWeights, [topic]: clamp(currentTopic + delta) }, - sourceWeights: { - ...prefs.sourceWeights, - [sourceSlug]: clamp(currentSource + delta), - }, - }; -} diff --git a/apps/mana/apps/web/src/lib/modules/news/index.ts b/apps/mana/apps/web/src/lib/modules/news/index.ts deleted file mode 100644 index e041a5453..000000000 --- a/apps/mana/apps/web/src/lib/modules/news/index.ts +++ /dev/null @@ -1,65 +0,0 @@ -/** - * News module — barrel exports. - */ - -export { - articleTable, - categoryTable, - preferencesTable, - reactionTable, - cachedFeedTable, - DEFAULT_PREFERENCES, -} from './collections'; - -export { - useSavedArticles, - useArticle, - useCategories, - usePreferences, - useReactions, - useCachedFeed, - toArticle, - toCategory, - toPreferences, - toReaction, - formatRelativeTime, -} from './queries'; - -export { - rankFeed, - scoreArticle, - buildReactedIds, - buildReactionSets, - applyReaction, - TOPIC_WEIGHT_DEFAULT, -} from './feed-engine'; - -export type { ScoredArticle, ScoreContext, WeightDiff } from './feed-engine'; - -export { articlesStore } from './stores/articles.svelte'; -export { categoriesStore } from './stores/categories.svelte'; -export { preferencesStore } from './stores/preferences.svelte'; -export { reactionsStore } from './stores/reactions.svelte'; -export { feedCacheStore } from './stores/feed-cache.svelte'; - -export { fetchFeed } from './api'; -export type { FeedArticleDto, FeedQuery } from './api'; - -export { SOURCES_META, SOURCE_META_BY_SLUG, sourcesForTopic, TOPIC_LABELS } from './sources-meta'; -export type { SourceMeta } from './sources-meta'; - -export { ALL_TOPICS, PREFERENCES_ID } from './types'; -export type { - Article, - Category, - LocalArticle, - LocalCachedArticle, - LocalCategory, - LocalPreferences, - LocalReaction, - Language, - Preferences, - Reaction, - ReactionKind, - Topic, -} from './types'; diff --git a/apps/mana/apps/web/src/lib/modules/news/module.config.ts b/apps/mana/apps/web/src/lib/modules/news/module.config.ts deleted file mode 100644 index 990130948..000000000 --- a/apps/mana/apps/web/src/lib/modules/news/module.config.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { ModuleConfig } from '$lib/data/module-registry'; - -/** - * News module — five Dexie tables, four of them synced. - * - * `newsCachedFeed` is intentionally absent: it mirrors the public - * server pool, refreshes on a 10-minute poll, and would chew through - * sync bandwidth + storage quota for zero benefit (the same data is - * just an HTTP fetch away). - */ -export const newsModuleConfig: ModuleConfig = { - appId: 'news', - tables: [ - { name: 'newsArticles', syncName: 'articles' }, - { name: 'newsCategories', syncName: 'categories' }, - { name: 'newsPreferences', syncName: 'preferences' }, - { name: 'newsReactions', syncName: 'reactions' }, - ], -}; diff --git a/apps/mana/apps/web/src/lib/modules/news/queries.ts b/apps/mana/apps/web/src/lib/modules/news/queries.ts deleted file mode 100644 index 41cb86f52..000000000 --- a/apps/mana/apps/web/src/lib/modules/news/queries.ts +++ /dev/null @@ -1,182 +0,0 @@ -import { formatDate } from '$lib/i18n/format'; -import { deriveUpdatedAt } from '$lib/data/sync'; -/** - * Reactive queries + type converters for News. - * - * Read-side only. Anything that mutates lives in stores/*.svelte.ts. - */ - -import { useLiveQueryWithDefault } from '@mana/local-store/svelte'; -import { db } from '$lib/data/database'; -import { decryptRecords } from '$lib/data/crypto'; -import { - articleTable, - cachedFeedTable, - categoryTable, - preferencesTable, - reactionTable, - DEFAULT_PREFERENCES, -} from './collections'; -import type { - Article, - Category, - LocalArticle, - LocalCachedArticle, - LocalCategory, - LocalPreferences, - LocalReaction, - Preferences, - Reaction, -} from './types'; -import { PREFERENCES_ID } from './types'; - -// ─── Type converters ─────────────────────────────────────── - -export function toArticle(local: LocalArticle): Article { - return { - id: local.id, - type: local.type, - sourceCuratedId: local.sourceCuratedId ?? undefined, - originalUrl: local.originalUrl, - title: local.title, - excerpt: local.excerpt, - content: local.content, - htmlContent: local.htmlContent, - author: local.author, - siteName: local.siteName, - sourceSlug: local.sourceSlug, - imageUrl: local.imageUrl, - categoryId: local.categoryId, - wordCount: local.wordCount, - readingTimeMinutes: local.readingTimeMinutes, - publishedAt: local.publishedAt, - isArchived: local.isArchived, - isRead: local.isRead, - isFavorite: local.isFavorite, - createdAt: local.createdAt ?? new Date().toISOString(), - updatedAt: deriveUpdatedAt(local), - }; -} - -export function toCategory(local: LocalCategory): Category { - return { - id: local.id, - name: local.name, - color: local.color, - icon: local.icon, - sortOrder: local.sortOrder, - createdAt: local.createdAt ?? new Date().toISOString(), - updatedAt: deriveUpdatedAt(local), - }; -} - -export function toPreferences(local: LocalPreferences): Preferences { - // Force the array fields back to arrays even if decryption left an - // encrypted blob string in place (vault locked at boot). Without this - // guard `{#each prefs.selectedTopics}` iterates the encrypted string - // char-by-char and crashes `TOPIC_LABELS[topic].emoji` on render. - return { - id: local.id, - selectedTopics: Array.isArray(local.selectedTopics) ? local.selectedTopics : [], - blockedSources: Array.isArray(local.blockedSources) ? local.blockedSources : [], - preferredLanguages: Array.isArray(local.preferredLanguages) - ? local.preferredLanguages - : ['de', 'en'], - topicWeights: - local.topicWeights && typeof local.topicWeights === 'object' ? local.topicWeights : {}, - sourceWeights: - local.sourceWeights && typeof local.sourceWeights === 'object' ? local.sourceWeights : {}, - onboardingCompleted: local.onboardingCompleted ?? false, - customFeeds: Array.isArray(local.customFeeds) ? local.customFeeds : [], - }; -} - -export function toReaction(local: LocalReaction): Reaction { - return { - id: local.id, - articleId: local.articleId, - reaction: local.reaction, - sourceSlug: local.sourceSlug, - topic: local.topic, - createdAt: local.createdAt ?? new Date().toISOString(), - }; -} - -// ─── Live queries ────────────────────────────────────────── - -/** Saved articles (the personal reading list). Encrypted on disk. */ -export function useSavedArticles() { - return useLiveQueryWithDefault(async () => { - const visible = (await articleTable.toArray()).filter((a) => !a.deletedAt && !a.isArchived); - const decrypted = await decryptRecords('newsArticles', visible); - return decrypted.map(toArticle).sort((a, b) => b.createdAt.localeCompare(a.createdAt)); - }, [] as Article[]); -} - -export function useArticle(id: string) { - return useLiveQueryWithDefault( - async () => { - const local = await articleTable.get(id); - if (!local || local.deletedAt) return null; - const [decrypted] = await decryptRecords('newsArticles', [local]); - return decrypted ? toArticle(decrypted) : null; - }, - null as Article | null - ); -} - -export function useCategories() { - return useLiveQueryWithDefault(async () => { - const visible = (await categoryTable.toArray()).filter((c) => !c.deletedAt); - const decrypted = await decryptRecords('newsCategories', visible); - return decrypted.map(toCategory).sort((a, b) => a.sortOrder - b.sortOrder); - }, [] as Category[]); -} - -/** - * Singleton preferences row. Returns the default-shape preferences if - * the user has never opened the module before — onboardingCompleted - * starts as `false`, which the route layer uses to redirect into the - * onboarding view on first launch. - */ -export function usePreferences() { - return useLiveQueryWithDefault(async () => { - const local = await preferencesTable.get(PREFERENCES_ID); - if (!local) return toPreferences(DEFAULT_PREFERENCES); - const [decrypted] = await decryptRecords('newsPreferences', [local]); - return toPreferences(decrypted ?? DEFAULT_PREFERENCES); - }, toPreferences(DEFAULT_PREFERENCES)); -} - -export function useReactions() { - return useLiveQueryWithDefault(async () => { - const visible = (await reactionTable.toArray()).filter((r) => !r.deletedAt); - const decrypted = await decryptRecords('newsReactions', visible); - return decrypted.map(toReaction); - }, [] as Reaction[]); -} - -/** The local mirror of the server's curated pool — plaintext, not synced. */ -export function useCachedFeed() { - return useLiveQueryWithDefault(async () => { - const all = await cachedFeedTable.toArray(); - // Newest first, but the feed engine re-sorts by score so this is - // only a stable input order. - return all.sort((a, b) => (b.publishedAt ?? '').localeCompare(a.publishedAt ?? '')); - }, [] as LocalCachedArticle[]); -} - -// ─── Pure helpers ────────────────────────────────────────── - -export function formatRelativeTime(iso: string | null): string { - if (!iso) return ''; - const diff = Date.now() - new Date(iso).getTime(); - const mins = Math.floor(diff / 60000); - if (mins < 1) return 'jetzt'; - if (mins < 60) return `vor ${mins}m`; - const hours = Math.floor(mins / 60); - if (hours < 24) return `vor ${hours}h`; - const days = Math.floor(hours / 24); - if (days < 7) return `vor ${days}d`; - return formatDate(new Date(iso), { day: 'numeric', month: 'short' }); -} diff --git a/apps/mana/apps/web/src/lib/modules/news/sources-meta.ts b/apps/mana/apps/web/src/lib/modules/news/sources-meta.ts deleted file mode 100644 index 670045dec..000000000 --- a/apps/mana/apps/web/src/lib/modules/news/sources-meta.ts +++ /dev/null @@ -1,83 +0,0 @@ -/** - * Source metadata for the News onboarding picker and per-article badges. - * - * MUST stay in sync with `services/news-ingester/src/sources.ts` — - * the `slug` is the cross-reference key (user blocklists store it - * verbatim, articles in the curated pool reference it). Adding or - * removing a source means editing both files. - * - * `language` and `topic` are duplicated from the ingester so the client - * doesn't need to fetch source metadata at runtime. - */ - -import type { Topic, Language } from './types'; - -export interface SourceMeta { - slug: string; - name: string; - topic: Topic; - language: Language; -} - -export const SOURCES_META: readonly SourceMeta[] = [ - // tech - { slug: 'hacker-news', name: 'Hacker News', topic: 'tech', language: 'en' }, - { slug: 'arstechnica', name: 'Ars Technica', topic: 'tech', language: 'en' }, - { slug: 'theverge', name: 'The Verge', topic: 'tech', language: 'en' }, - { slug: 'heise', name: 'heise online', topic: 'tech', language: 'de' }, - // wissenschaft - { slug: 'quanta-magazine', name: 'Quanta Magazine', topic: 'wissenschaft', language: 'en' }, - { slug: 'spektrum', name: 'Spektrum', topic: 'wissenschaft', language: 'de' }, - { slug: 'nature-news', name: 'Nature News', topic: 'wissenschaft', language: 'en' }, - { slug: 'phys-org', name: 'Phys.org', topic: 'wissenschaft', language: 'en' }, - // weltgeschehen - { slug: 'tagesschau', name: 'Tagesschau', topic: 'weltgeschehen', language: 'de' }, - { slug: 'bbc-world', name: 'BBC World', topic: 'weltgeschehen', language: 'en' }, - { slug: 'aljazeera', name: 'Al Jazeera', topic: 'weltgeschehen', language: 'en' }, - { slug: 'dw-top', name: 'Deutsche Welle', topic: 'weltgeschehen', language: 'en' }, - // wirtschaft - { slug: 'handelsblatt', name: 'Handelsblatt', topic: 'wirtschaft', language: 'de' }, - { slug: 'ft-world', name: 'Financial Times', topic: 'wirtschaft', language: 'en' }, - { slug: 'bloomberg-markets', name: 'Bloomberg Markets', topic: 'wirtschaft', language: 'en' }, - { - slug: 'economist-finance', - name: 'The Economist — Finance', - topic: 'wirtschaft', - language: 'en', - }, - // kultur - { slug: 'guardian-culture', name: 'The Guardian Culture', topic: 'kultur', language: 'en' }, - { slug: 'guardian-books', name: 'The Guardian Books', topic: 'kultur', language: 'en' }, - { slug: 'npr-arts', name: 'NPR Arts', topic: 'kultur', language: 'en' }, - // gesundheit - { slug: 'stat-news', name: 'STAT News', topic: 'gesundheit', language: 'en' }, - { slug: 'bbc-health', name: 'BBC Health', topic: 'gesundheit', language: 'en' }, - { slug: 'sciencedaily-health', name: 'ScienceDaily Health', topic: 'gesundheit', language: 'en' }, - // politik - { slug: 'spiegel-politik', name: 'Spiegel Politik', topic: 'politik', language: 'de' }, - { slug: 'politico-eu', name: 'Politico EU', topic: 'politik', language: 'en' }, - { - slug: 'atlantic-politics', - name: 'The Atlantic — Politics', - topic: 'politik', - language: 'en', - }, -]; - -export const SOURCE_META_BY_SLUG: Record = Object.fromEntries( - SOURCES_META.map((s) => [s.slug, s]) -); - -export function sourcesForTopic(topic: Topic): readonly SourceMeta[] { - return SOURCES_META.filter((s) => s.topic === topic); -} - -export const TOPIC_LABELS: Record = { - tech: { de: 'Tech', en: 'Tech', emoji: '💻' }, - wissenschaft: { de: 'Wissenschaft', en: 'Science', emoji: '🔬' }, - weltgeschehen: { de: 'Weltgeschehen', en: 'World', emoji: '🌍' }, - wirtschaft: { de: 'Wirtschaft', en: 'Business', emoji: '📈' }, - kultur: { de: 'Kultur', en: 'Culture', emoji: '🎭' }, - gesundheit: { de: 'Gesundheit', en: 'Health', emoji: '🩺' }, - politik: { de: 'Politik', en: 'Politics', emoji: '🏛️' }, -}; 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 deleted file mode 100644 index 479616c0c..000000000 --- a/apps/mana/apps/web/src/lib/modules/news/stores/articles.svelte.ts +++ /dev/null @@ -1,92 +0,0 @@ -/** - * Articles store — the user's saved reading list. - * - * 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`. - */ - -import { encryptRecord } from '$lib/data/crypto'; -import { emitDomainEvent } from '$lib/data/events'; -import { articleTable } from '../collections'; -import { toArticle } from '../queries'; -import type { Article, LocalArticle, LocalCachedArticle } from '../types'; - -export const articlesStore = { - async saveFromCurated(input: LocalCachedArticle): Promise
{ - // Dedupe: if the user has already saved this curated article, - // return the existing row instead of creating a duplicate. The - // `sourceCuratedId` index makes this O(1). - const existing = await articleTable.where('sourceCuratedId').equals(input.id).first(); - if (existing) return toArticle(existing); - - const newLocal: LocalArticle = { - id: crypto.randomUUID(), - type: 'curated', - sourceCuratedId: input.id, - originalUrl: input.originalUrl, - title: input.title, - excerpt: input.excerpt, - content: input.content, - htmlContent: input.htmlContent, - author: input.author, - siteName: input.siteName, - sourceSlug: input.sourceSlug, - imageUrl: input.imageUrl, - categoryId: null, - wordCount: input.wordCount, - readingTimeMinutes: input.readingTimeMinutes, - publishedAt: input.publishedAt, - isArchived: false, - isRead: false, - isFavorite: false, - }; - const snapshot = toArticle(newLocal); - await encryptRecord('newsArticles', newLocal); - await articleTable.add(newLocal); - emitDomainEvent('ArticleSaved', 'news', 'newsArticles', newLocal.id, { - articleId: newLocal.id, - title: input.title ?? '', - }); - return snapshot; - }, - - async markRead(id: string, isRead = true): Promise { - await articleTable.update(id, { - isRead, - }); - }, - - async toggleFavorite(id: string): Promise { - const a = await articleTable.get(id); - if (!a) return; - await articleTable.update(id, { - isFavorite: !a.isFavorite, - }); - }, - - async archive(id: string): Promise { - await articleTable.update(id, { - isArchived: true, - }); - }, - - async setCategory(id: string, categoryId: string | null): Promise { - await articleTable.update(id, { - categoryId, - }); - }, - - async delete(id: string): Promise { - await articleTable.update(id, { - deletedAt: new Date().toISOString(), - }); - }, -}; diff --git a/apps/mana/apps/web/src/lib/modules/news/stores/categories.svelte.ts b/apps/mana/apps/web/src/lib/modules/news/stores/categories.svelte.ts deleted file mode 100644 index 876d91b9c..000000000 --- a/apps/mana/apps/web/src/lib/modules/news/stores/categories.svelte.ts +++ /dev/null @@ -1,89 +0,0 @@ -/** - * Categories store — user-defined folders for the saved reading list. - * - * Categories live in `newsCategories`. The link from an article to its - * category is `LocalArticle.categoryId` (a plaintext FK index), set - * via `articlesStore.setCategory`. - * - * Default seeds (Lese später / Recherche) are NOT created here — the - * user starts with zero categories and adds them on demand. Empty is - * a valid state and the saved-list view falls back to "Alle Artikel" - * when no category is selected. - */ - -import { encryptRecord } from '$lib/data/crypto'; -import { categoryTable } from '../collections'; -import type { LocalCategory } from '../types'; - -const DEFAULT_COLORS = [ - '#3b82f6', - '#22c55e', - '#f59e0b', - '#ec4899', - '#8b5cf6', - '#06b6d4', - '#f43f5e', - '#84cc16', -]; - -function pickColor(existingCount: number): string { - return DEFAULT_COLORS[existingCount % DEFAULT_COLORS.length]; -} - -export const categoriesStore = { - async create(input: { name: string; color?: string; icon?: string }): Promise { - const count = await categoryTable.count(); - const newLocal: LocalCategory = { - id: crypto.randomUUID(), - name: input.name.trim() || 'Ohne Namen', - color: input.color ?? pickColor(count), - icon: input.icon ?? '📁', - sortOrder: count, - }; - await encryptRecord('newsCategories', newLocal); - await categoryTable.add(newLocal); - return newLocal; - }, - - async rename(id: string, name: string): Promise { - const trimmed = name.trim(); - if (!trimmed) return; - const diff: Partial = { - name: trimmed, - }; - await encryptRecord('newsCategories', diff); - await categoryTable.update(id, diff); - }, - - async setColor(id: string, color: string): Promise { - await categoryTable.update(id, { - color, - }); - }, - - async setIcon(id: string, icon: string): Promise { - await categoryTable.update(id, { - icon, - }); - }, - - async reorder(ids: string[]): Promise { - // Bulk update via individual writes — Dexie has no native bulkUpdate - // for partial diffs and the per-call cost is negligible at folder - // counts (typically <20). - const now = new Date().toISOString(); - for (let i = 0; i < ids.length; i++) { - await categoryTable.update(ids[i], { sortOrder: i }); - } - }, - - async delete(id: string): Promise { - // Soft-delete the category itself. Articles that referenced it - // keep `categoryId` pointing at the tombstoned row — the saved - // view treats unknown categoryIds as "uncategorized" so they - // don't disappear. A subsequent re-categorize cleans them up. - await categoryTable.update(id, { - deletedAt: new Date().toISOString(), - }); - }, -}; diff --git a/apps/mana/apps/web/src/lib/modules/news/stores/feed-cache.svelte.ts b/apps/mana/apps/web/src/lib/modules/news/stores/feed-cache.svelte.ts deleted file mode 100644 index ae9680986..000000000 --- a/apps/mana/apps/web/src/lib/modules/news/stores/feed-cache.svelte.ts +++ /dev/null @@ -1,120 +0,0 @@ -/** - * Feed-cache store — pulls the curated pool from /api/v1/news/feed - * into the local-only `newsCachedFeed` table. - * - * Why a local mirror at all? The feed engine (scoreArticle, rankFeed) - * runs against the cached pool every time the feed view re-renders. - * Hitting the network on every render would be silly; hitting the - * network on every preferences change would be worse. Caching also - * gives us offline reading for the cards the user already saw. - * - * The cache is bounded: we keep at most CACHE_LIMIT rows, and prune - * the oldest by ingestedAt before each refresh. The bounded size + the - * fact that the cache is plaintext + not synced is what justifies - * leaving it out of the encryption registry and the sync map. - * - * `start()` should be called once from the news +layout — it kicks an - * immediate refresh and then polls on a 10-minute interval. The - * interval is held in module scope on purpose so multiple route entries - * can't accidentally double up. - */ - -import { cachedFeedTable } from '../collections'; -import { fetchFeed } from '../api'; -import type { FeedArticleDto } from '../api'; -import type { LocalCachedArticle } from '../types'; - -const POLL_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes -const CACHE_LIMIT = 400; - -let pollHandle: ReturnType | null = null; -let inFlight = false; -let lastError: string | null = null; -let lastRefreshedAt: string | null = null; - -function toLocal(dto: FeedArticleDto): LocalCachedArticle { - return { - id: dto.id, - originalUrl: dto.originalUrl, - title: dto.title, - excerpt: dto.excerpt, - content: dto.content, - htmlContent: dto.htmlContent, - author: dto.author, - siteName: dto.siteName, - sourceSlug: dto.sourceSlug, - imageUrl: dto.imageUrl, - topic: dto.topic, - language: dto.language, - wordCount: dto.wordCount, - readingTimeMinutes: dto.readingTimeMinutes, - publishedAt: dto.publishedAt, - ingestedAt: dto.ingestedAt, - cachedAt: new Date().toISOString(), - }; -} - -async function pruneToLimit(): Promise { - const count = await cachedFeedTable.count(); - if (count <= CACHE_LIMIT) return; - // Keep the newest CACHE_LIMIT rows by ingestedAt. Dexie has no - // LIMIT/OFFSET on plain table, so collect all PKs sorted and slice. - const all = await cachedFeedTable.toArray(); - all.sort((a, b) => (b.ingestedAt ?? '').localeCompare(a.ingestedAt ?? '')); - const toDelete = all.slice(CACHE_LIMIT).map((a) => a.id); - if (toDelete.length > 0) await cachedFeedTable.bulkDelete(toDelete); -} - -export const feedCacheStore = { - get lastError() { - return lastError; - }, - get lastRefreshedAt() { - return lastRefreshedAt; - }, - get inFlight() { - return inFlight; - }, - - async refresh(opts: { topics?: string[]; lang?: 'de' | 'en' | 'all' } = {}): Promise { - if (inFlight) return; - inFlight = true; - lastError = null; - try { - const dtos = await fetchFeed({ - limit: 200, - topics: opts.topics, - lang: opts.lang ?? 'all', - }); - if (dtos.length > 0) { - // bulkPut keeps existing rows for the same id and updates - // them in place. New rows from the server replace the - // previous mirror, old rows that fell out of the server's - // 200-row window stay in the cache until pruneToLimit cuts - // them. That's the behavior we want — the cache should - // degrade gradually, not flush every refresh. - await cachedFeedTable.bulkPut(dtos.map(toLocal)); - } - await pruneToLimit(); - lastRefreshedAt = new Date().toISOString(); - } catch (err) { - lastError = err instanceof Error ? err.message : String(err); - console.warn('[news] feed refresh failed:', lastError); - } finally { - inFlight = false; - } - }, - - start(opts: { topics?: string[]; lang?: 'de' | 'en' | 'all' } = {}): void { - if (pollHandle) return; // already started - void this.refresh(opts); - pollHandle = setInterval(() => void this.refresh(opts), POLL_INTERVAL_MS); - }, - - stop(): void { - if (pollHandle) { - clearInterval(pollHandle); - pollHandle = null; - } - }, -}; diff --git a/apps/mana/apps/web/src/lib/modules/news/stores/preferences.svelte.ts b/apps/mana/apps/web/src/lib/modules/news/stores/preferences.svelte.ts deleted file mode 100644 index c75e323c6..000000000 --- a/apps/mana/apps/web/src/lib/modules/news/stores/preferences.svelte.ts +++ /dev/null @@ -1,137 +0,0 @@ -/** - * Preferences store — singleton row keyed on `PREFERENCES_ID`. - * - * The first read of the preferences row is also the place that creates - * it on disk, so the rest of the codebase can assume it always exists. - * Onboarding then flips `onboardingCompleted` to true. - */ - -import { encryptRecord } from '$lib/data/crypto'; -import { preferencesTable, DEFAULT_PREFERENCES } from '../collections'; -import { toPreferences } from '../queries'; -import type { CustomFeed, LocalPreferences, Preferences, Topic, Language } from '../types'; -import { PREFERENCES_ID } from '../types'; - -async function ensureRow(): Promise { - const existing = await preferencesTable.get(PREFERENCES_ID); - if (existing) return existing; - const fresh: LocalPreferences = { ...DEFAULT_PREFERENCES }; - await encryptRecord('newsPreferences', fresh); - await preferencesTable.add(fresh); - return fresh; -} - -export const preferencesStore = { - async get(): Promise { - const row = await ensureRow(); - return toPreferences(row); - }, - - async completeOnboarding(input: { - topics: Topic[]; - languages: Language[]; - blockedSources?: string[]; - }): Promise { - await ensureRow(); - // Spread the input arrays — callers in onboarding pass Svelte 5 - // `$state` proxy arrays, which IndexedDB cannot structured-clone - // (DataCloneError on the Dexie hook's _pendingChanges write). - const diff: Partial = { - selectedTopics: [...input.topics], - preferredLanguages: [...input.languages], - blockedSources: [...(input.blockedSources ?? [])], - onboardingCompleted: true, - }; - await encryptRecord('newsPreferences', diff); - await preferencesTable.update(PREFERENCES_ID, diff); - }, - - async setTopics(topics: Topic[]): Promise { - await ensureRow(); - const diff: Partial = { - selectedTopics: [...topics], - }; - await encryptRecord('newsPreferences', diff); - await preferencesTable.update(PREFERENCES_ID, diff); - }, - - async setLanguages(languages: Language[]): Promise { - await ensureRow(); - const diff: Partial = { - preferredLanguages: [...languages], - }; - await encryptRecord('newsPreferences', diff); - await preferencesTable.update(PREFERENCES_ID, diff); - }, - - async toggleBlockedSource(slug: string): Promise { - const row = await ensureRow(); - const blocked = row.blockedSources ?? []; - const next = blocked.includes(slug) ? blocked.filter((s) => s !== slug) : [...blocked, slug]; - const diff: Partial = { - blockedSources: next, - }; - await encryptRecord('newsPreferences', diff); - await preferencesTable.update(PREFERENCES_ID, diff); - }, - - /** - * Apply a precomputed weight diff (from feed-engine.applyReaction). - * Merges with existing weights — caller already did the math. - */ - async applyWeightDiff(diff: { - topicWeights?: Record; - sourceWeights?: Record; - blockedSources?: string[]; - }): Promise { - await ensureRow(); - const update: Partial = { - ...diff, - }; - await encryptRecord('newsPreferences', update); - await preferencesTable.update(PREFERENCES_ID, update); - }, - - async pinCustomFeed(feed: { url: string; title: string; topic?: Topic }): Promise { - const row = await ensureRow(); - const existing = Array.isArray(row.customFeeds) ? row.customFeeds : []; - if (existing.some((f) => f.url === feed.url)) return; - const next: CustomFeed[] = [ - ...existing, - { - id: crypto.randomUUID(), - url: feed.url, - title: feed.title, - topic: feed.topic, - pinnedAt: Date.now(), - }, - ]; - const diff: Partial = { - customFeeds: next, - }; - await encryptRecord('newsPreferences', diff); - await preferencesTable.update(PREFERENCES_ID, diff); - }, - - async unpinCustomFeed(id: string): Promise { - const row = await ensureRow(); - const existing = Array.isArray(row.customFeeds) ? row.customFeeds : []; - const next = existing.filter((f) => f.id !== id); - if (next.length === existing.length) return; - const diff: Partial = { - customFeeds: next, - }; - await encryptRecord('newsPreferences', diff); - await preferencesTable.update(PREFERENCES_ID, diff); - }, - - async resetWeights(): Promise { - await ensureRow(); - const diff: Partial = { - topicWeights: {}, - sourceWeights: {}, - }; - await encryptRecord('newsPreferences', diff); - await preferencesTable.update(PREFERENCES_ID, diff); - }, -}; diff --git a/apps/mana/apps/web/src/lib/modules/news/stores/reactions.svelte.ts b/apps/mana/apps/web/src/lib/modules/news/stores/reactions.svelte.ts deleted file mode 100644 index bcf556da6..000000000 --- a/apps/mana/apps/web/src/lib/modules/news/stores/reactions.svelte.ts +++ /dev/null @@ -1,61 +0,0 @@ -/** - * Reactions store — records the user's per-article feedback and pipes - * the matching weight delta into the preferences store in the same - * call. Two writes, one logical operation: - * - * 1. add a `newsReactions` row (drops the article from `reactedIds` - * so the feed engine stops surfacing it) - * 2. apply the weight diff back to `newsPreferences` - * - * The reaction row stays around so undo / "show what I dismissed" - * stays cheap. The preferences diff is what makes the suppression - * persist across cache refreshes. - */ - -import { encryptRecord } from '$lib/data/crypto'; -import { reactionTable } from '../collections'; -import { applyReaction as computeWeightDiff } from '../feed-engine'; -import { preferencesStore } from './preferences.svelte'; -import type { LocalReaction, ReactionKind } from '../types'; - -async function loadCurrentPrefs() { - return preferencesStore.get(); -} - -export const reactionsStore = { - async react(input: { - articleId: string; - reaction: ReactionKind; - topic: string; - sourceSlug: string; - }): Promise { - const prefs = await loadCurrentPrefs(); - - // 1. Persist the reaction row. - const row: LocalReaction = { - id: crypto.randomUUID(), - articleId: input.articleId, - reaction: input.reaction, - topic: input.topic, - sourceSlug: input.sourceSlug, - }; - await encryptRecord('newsReactions', row); - await reactionTable.add(row); - - // 2. Update preferences (weight + blocklist) in lockstep. - const diff = computeWeightDiff(prefs, input.reaction, input.topic, input.sourceSlug); - if (Object.keys(diff).length > 0) { - await preferencesStore.applyWeightDiff(diff); - } - }, - - async undo(reactionId: string): Promise { - // Soft-delete: tombstone the reaction so the article shows up - // again in the feed. Weights stay where they were — undoing a - // thumbs-down doesn't *boost* the source, it just stops further - // suppression. - await reactionTable.update(reactionId, { - deletedAt: new Date().toISOString(), - }); - }, -}; diff --git a/apps/mana/apps/web/src/lib/modules/news/tools.ts b/apps/mana/apps/web/src/lib/modules/news/tools.ts deleted file mode 100644 index 99be9aa19..000000000 --- a/apps/mana/apps/web/src/lib/modules/news/tools.ts +++ /dev/null @@ -1,52 +0,0 @@ -/** - * News Tools — LLM-accessible operations for the news module. - * - * `save_news_article` is the agent's path into the user's reading list. - * 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 '$lib/modules/articles/stores/articles.svelte'; - -export const newsTools: ModuleTool[] = [ - { - name: 'save_news_article', - module: 'news', - description: - 'Speichert einen Artikel von einer URL in die Leseliste. URL wird serverseitig per Readability extrahiert.', - parameters: [ - { name: 'url', type: 'string', description: 'Die Artikel-URL', required: true }, - { - name: 'title', - type: 'string', - description: 'Anzeigetitel für den Approval-Dialog (informativ)', - required: false, - }, - { - name: 'summary', - type: 'string', - description: 'Kurze Begründung warum dieser Artikel relevant ist', - required: false, - }, - ], - async execute(params) { - const url = params.url as string; - const { article, duplicate } = await articlesStore.saveFromUrl(url); - return { - success: true, - 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/lib/modules/news/types.ts b/apps/mana/apps/web/src/lib/modules/news/types.ts deleted file mode 100644 index 472e652d0..000000000 --- a/apps/mana/apps/web/src/lib/modules/news/types.ts +++ /dev/null @@ -1,198 +0,0 @@ -/** - * News module types — local-first reading hub backed by the curated - * pool from `news.curated_articles` (see services/news-ingester). - * - * Local data is split across five Dexie tables: - * - * newsArticles — saved reading list (encrypted) - * newsCategories — user-defined folders for the reading list - * newsPreferences — singleton row: topics, blocklist, weights - * newsReactions — per-article feedback signals - * newsCachedFeed — local mirror of the server's curated pool - * (NOT synced, NOT encrypted) - */ - -import type { BaseRecord } from '@mana/local-store'; - -export type Topic = - | 'tech' - | 'wissenschaft' - | 'weltgeschehen' - | 'wirtschaft' - | 'kultur' - | 'gesundheit' - | 'politik'; - -export const ALL_TOPICS: readonly Topic[] = [ - 'tech', - 'wissenschaft', - 'weltgeschehen', - 'wirtschaft', - 'kultur', - 'gesundheit', - 'politik', -]; - -export type Language = 'de' | 'en'; - -export type ReactionKind = 'interested' | 'not_interested' | 'source_blocked' | 'hidden'; - -// ─── Saved reading list ──────────────────────────────────── - -export interface LocalArticle extends BaseRecord { - /** 'curated' = saved from the server pool, 'saved' = ad-hoc URL extract. */ - type: 'curated' | 'saved'; - /** Foreign key into the server's curated pool when type='curated'. */ - sourceCuratedId?: string | null; - originalUrl: string; - title: string; - excerpt: string | null; - content: string; - htmlContent: string | null; - author: string | null; - siteName: string | null; - sourceSlug: string | null; - imageUrl: string | null; - categoryId: string | null; - wordCount: number | null; - readingTimeMinutes: number | null; - publishedAt: string | null; - isArchived: boolean; - isRead: boolean; - isFavorite: boolean; -} - -export interface LocalCategory extends BaseRecord { - name: string; - color: string; - icon: string; - sortOrder: number; -} - -// ─── Preferences (singleton) ─────────────────────────────── - -/** - * The single row id for the preferences singleton — there is exactly - * one preferences row per user, so we use a stable string instead of a - * uuid to make upserts idempotent. - */ -export const PREFERENCES_ID = 'singleton'; - -export interface CustomFeed { - id: string; - url: string; - title: string; - /** Optional topic tag from the standard taxonomy. */ - topic?: Topic; - /** Epoch ms when the user pinned this feed. */ - pinnedAt: number; -} - -export interface LocalPreferences extends BaseRecord { - id: string; - selectedTopics: Topic[]; - blockedSources: string[]; - preferredLanguages: Language[]; - /** topic slug → weight (default 1.0, range ~0.1 to 3.0). */ - topicWeights: Record; - /** source slug → weight (default 1.0, range ~0.1 to 3.0). */ - sourceWeights: Record; - onboardingCompleted: boolean; - /** - * User-subscribed RSS feeds, populated from the News Research module's - * "Pin feed" action. Not ingested centrally — the client fetches these - * on its own schedule (see feed-cache). - */ - customFeeds?: CustomFeed[]; -} - -// ─── Reactions ───────────────────────────────────────────── - -export interface LocalReaction extends BaseRecord { - /** The curated article id (server-side uuid from the pool). */ - articleId: string; - reaction: ReactionKind; - /** Denormalized for O(1) weight updates without a join. */ - sourceSlug: string; - topic: string; -} - -// ─── Cached pool mirror (local only) ─────────────────────── - -export interface LocalCachedArticle { - /** Server-side curated_articles.id. Used as the dedupe key. */ - id: string; - originalUrl: string; - title: string; - excerpt: string | null; - content: string; - htmlContent: string | null; - author: string | null; - siteName: string; - sourceSlug: string; - imageUrl: string | null; - topic: string; - language: string; - wordCount: number | null; - readingTimeMinutes: number | null; - publishedAt: string | null; - ingestedAt: string; - /** Local timestamp when this row entered the cache. */ - cachedAt: string; -} - -// ─── Public DTOs (rendered by views) ─────────────────────── - -export interface Article { - id: string; - type: 'curated' | 'saved'; - sourceCuratedId?: string; - originalUrl: string; - title: string; - excerpt: string | null; - content: string; - htmlContent: string | null; - author: string | null; - siteName: string | null; - sourceSlug: string | null; - imageUrl: string | null; - categoryId: string | null; - wordCount: number | null; - readingTimeMinutes: number | null; - publishedAt: string | null; - isArchived: boolean; - isRead: boolean; - isFavorite: boolean; - createdAt: string; - updatedAt: string; -} - -export interface Category { - id: string; - name: string; - color: string; - icon: string; - sortOrder: number; - createdAt: string; - updatedAt: string; -} - -export interface Preferences { - id: string; - selectedTopics: Topic[]; - blockedSources: string[]; - preferredLanguages: Language[]; - topicWeights: Record; - sourceWeights: Record; - onboardingCompleted: boolean; - customFeeds: CustomFeed[]; -} - -export interface Reaction { - id: string; - articleId: string; - reaction: ReactionKind; - sourceSlug: string; - topic: string; - createdAt: string; -} diff --git a/apps/mana/apps/web/src/lib/modules/news/widgets/NewsUnreadWidget.svelte b/apps/mana/apps/web/src/lib/modules/news/widgets/NewsUnreadWidget.svelte deleted file mode 100644 index 77e8d9afe..000000000 --- a/apps/mana/apps/web/src/lib/modules/news/widgets/NewsUnreadWidget.svelte +++ /dev/null @@ -1,129 +0,0 @@ - - -
-
-

- - News -

- Alle → -
- - {#if loading} -
- {#each Array(3) as _} -
- {/each} -
- {:else if !onboardingDone} -
-

Richte deinen Newsfeed ein.

- - Jetzt starten - -
- {:else if topThree.length === 0} -
-

Keine neuen Artikel.

- - Feed öffnen - -
- {:else} - - {/if} -
diff --git a/apps/mana/apps/web/src/lib/types/dashboard.ts b/apps/mana/apps/web/src/lib/types/dashboard.ts index ceb4fced9..b536f21ca 100644 --- a/apps/mana/apps/web/src/lib/types/dashboard.ts +++ b/apps/mana/apps/web/src/lib/types/dashboard.ts @@ -27,7 +27,6 @@ export type WidgetType = | 'day-timeline' // TimeBlocks: chronological day timeline | '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 @@ -306,14 +305,6 @@ export const WIDGET_REGISTRY: WidgetMeta[] = [ allowMultiple: false, requiredBackend: 'period', }, - { - type: 'news-unread', - nameKey: 'dashboard.widgets.news_unread.title', - descriptionKey: 'dashboard.widgets.news_unread.description', - icon: '📰', - defaultSize: 'small', - allowMultiple: false, - }, { type: 'articles-unread', nameKey: 'dashboard.widgets.articles_unread.title', diff --git a/apps/mana/apps/web/src/routes/(app)/+layout.svelte b/apps/mana/apps/web/src/routes/(app)/+layout.svelte index 4ec019630..39fb7acc3 100644 --- a/apps/mana/apps/web/src/routes/(app)/+layout.svelte +++ b/apps/mana/apps/web/src/routes/(app)/+layout.svelte @@ -16,7 +16,6 @@ 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, @@ -599,10 +598,6 @@ // 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 5ac8f0952..3c689c2da 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 @@ -9,8 +9,6 @@ import { goto } from '$app/navigation'; import { researchSessionStore } from '$lib/modules/news-research/stores/session.svelte'; import { articlesStore } from '$lib/modules/articles/stores/articles.svelte'; - import { preferencesStore } from '$lib/modules/news/stores/preferences.svelte'; - import { usePreferences } from '$lib/modules/news/queries'; import { RoutePage } from '$lib/components/shell'; let mode = $state<'query' | 'site'>('query'); @@ -22,20 +20,6 @@ let saveError = $state(null); const store = researchSessionStore; - const prefs$ = usePreferences(); - const pinnedUrls = $derived(new Set((prefs$.value?.customFeeds ?? []).map((f) => f.url))); - - async function togglePin(feed: { url: string; title: string | null }) { - if (pinnedUrls.has(feed.url)) { - const existing = (prefs$.value?.customFeeds ?? []).find((f) => f.url === feed.url); - if (existing) await preferencesStore.unpinCustomFeed(existing.id); - } else { - await preferencesStore.pinCustomFeed({ - url: feed.url, - title: feed.title ?? feed.url, - }); - } - } function isUrl(s: string): boolean { try { @@ -175,18 +159,6 @@ {feed.title ?? feed.url} {feed.type} {#if feed.sourceHit}{feed.sourceHit}{/if} - {/each} @@ -372,21 +344,6 @@ font-size: 0.75rem; color: hsl(var(--color-muted-foreground)); } - .pin { - margin-left: auto; - background: transparent; - border: 1px solid hsl(var(--color-border)); - border-radius: 0.35rem; - padding: 0.15rem 0.55rem; - font-size: 0.75rem; - cursor: pointer; - color: hsl(var(--color-foreground)); - } - .pin.pinned { - background: hsl(var(--color-primary)); - color: hsl(var(--color-primary-foreground, 0 0% 100%)); - border-color: transparent; - } .result { padding: 0.75rem; border: 1px solid hsl(var(--color-border)); diff --git a/apps/mana/apps/web/src/routes/(app)/news/+layout.svelte b/apps/mana/apps/web/src/routes/(app)/news/+layout.svelte deleted file mode 100644 index 4f5649c08..000000000 --- a/apps/mana/apps/web/src/routes/(app)/news/+layout.svelte +++ /dev/null @@ -1,39 +0,0 @@ - - - -{@render children()} diff --git a/apps/mana/apps/web/src/routes/(app)/news/+page.svelte b/apps/mana/apps/web/src/routes/(app)/news/+page.svelte deleted file mode 100644 index dd1953bdd..000000000 --- a/apps/mana/apps/web/src/routes/(app)/news/+page.svelte +++ /dev/null @@ -1,777 +0,0 @@ - - - - - News — Mana - - - -
- {#if !isOnboarded} - -
-

{$_('news.onboarding.welcome')}

-

{$_('news.onboarding.intro')}

-
- -
- {$_('news.onboarding.stepTopics')} - {$_('news.onboarding.stepLanguage')} - {$_('news.onboarding.stepSources')} -
- - {#if onboardingStep === 1} -
-

{$_('news.onboarding.topicsTitle')}

-

{$_('news.onboarding.topicsHint')}

-
- {#each ALL_TOPICS as topic} - - {/each} -
-
- -
-
- {:else if onboardingStep === 2} -
-

{$_('news.onboarding.languageTitle')}

-
- - -
-
- - -
-
- {:else} -
-

{$_('news.onboarding.sourcesTitle')}

-

- {$_('news.onboarding.sourcesHint')} -

-
- {#each pickedTopics as topic} -
-

- {TOPIC_LABELS[topic].emoji} - {TOPIC_LABELS[topic].de} -

-
- {#each sourcesForTopic(topic) as src} - - {/each} -
-
- {/each} -
-
- - -
-
- {/if} - {:else} - -
-
-

{$_('news.feed.title')}

-
- {$_('news.feed.articles', { values: { count: ranked.length } })} - {#if feedCacheStore.lastError} - · {$_('news.feed.loadError')} - {/if} -
-
-
- - 📑 - -
-
- - -
- {#each prefs.selectedTopics as topic} - - {TOPIC_LABELS[topic].emoji} - {TOPIC_LABELS[topic].de} - - {/each} -
- - {#if ranked.length === 0} -
- {#if pool.length === 0} -

{$_('news.feed.loading')}

- {:else} -

{$_('news.feed.empty')}

-

{$_('news.feed.emptyHint')}

- {/if} -
- {:else} -
- {#each ranked as scored (scored.article.id)} - {@const article = scored.article} - {@const isSaved = interestedIds.has(article.id)} -
- {#if article.imageUrl} - - {/if} -
-
- {article.siteName} - · - {formatRelativeTime(article.publishedAt)} - {#if article.readingTimeMinutes} - · - {$_('news.feed.readingTimeMin', { - values: { n: article.readingTimeMinutes }, - })} - {/if} - {#if isSaved} - {$_('news.feed.savedBadgeText')} - {/if} -
- - {#if article.excerpt} -

{article.excerpt}

- {/if} -
- - - -
-
-
- {/each} -
- {/if} - {/if} -
-
- - diff --git a/apps/mana/apps/web/src/routes/(app)/news/[id]/+page.svelte b/apps/mana/apps/web/src/routes/(app)/news/[id]/+page.svelte deleted file mode 100644 index c90a27506..000000000 --- a/apps/mana/apps/web/src/routes/(app)/news/[id]/+page.svelte +++ /dev/null @@ -1,334 +0,0 @@ - - - - - {title || 'Lese-Ansicht'} — Mana - - - -
-
- -
- - - {#if loaded?.kind === 'curated'} - - {/if} -
- - {#if !loaded} -
Lade…
- {:else if loaded.kind === 'missing'} -
-

Artikel nicht gefunden.

- -
- {:else} -
- {#if meta?.imageUrl} - - {/if} -

{title}

-
- {#if meta?.siteName} - {meta.siteName} - {/if} - {#if meta?.author} - · - {meta.author} - {/if} - {#if meta?.publishedAt} - · - {formatRelativeTime(meta.publishedAt)} - {/if} - {#if meta?.readingTimeMinutes} - · - {meta.readingTimeMinutes} min - {/if} -
- - {#if html} - -
{@html html}
- {:else if plain} -
- {#each plain.split('\n\n') as para} -

{para}

- {/each} -
- {/if} - - {#if meta?.originalUrl} - - {/if} -
- {/if} -
-
- - diff --git a/apps/mana/apps/web/src/routes/(app)/news/add/+page.svelte b/apps/mana/apps/web/src/routes/(app)/news/add/+page.svelte deleted file mode 100644 index 4a7e83948..000000000 --- a/apps/mana/apps/web/src/routes/(app)/news/add/+page.svelte +++ /dev/null @@ -1,26 +0,0 @@ - - - - -

Verschoben nach /articles/add

-
- - diff --git a/apps/mana/apps/web/src/routes/(app)/news/preferences/+page.svelte b/apps/mana/apps/web/src/routes/(app)/news/preferences/+page.svelte deleted file mode 100644 index 3043aee70..000000000 --- a/apps/mana/apps/web/src/routes/(app)/news/preferences/+page.svelte +++ /dev/null @@ -1,242 +0,0 @@ - - - - - {$_('news.preferences.page_title_html')} - - - -
-
-
- {$_('news.preferences.title')} - {$_('news.preferences.subtitle')} -
-
- -
-

{$_('news.preferences.topicsHeading')}

-

{$_('news.preferences.topicsHint')}

-
- {#each ALL_TOPICS as topic} - - {/each} -
-
- -
-

{$_('news.preferences.languagesHeading')}

-
- - -
-
- -
-

{$_('news.preferences.sourcesHeading')}

-

- {@html $_('news.preferences.sourcesHintHtml', { - values: { count: prefs.blockedSources.length }, - })} -

- {$_('news.preferences.sourcesLinkArrow')} -
- -
-

{$_('news.preferences.weightsHeading')}

-

- {$_('news.preferences.weightsHint', { - values: { topics: topicWeightCount, sources: sourceWeightCount }, - })} -

- -
- -
-

{$_('news.preferences.onboardingHeading')}

-

{$_('news.preferences.onboardingHint')}

- -
-
-
- - 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 deleted file mode 100644 index 8a38f93fd..000000000 --- a/apps/mana/apps/web/src/routes/(app)/news/saved/+page.svelte +++ /dev/null @@ -1,26 +0,0 @@ - - - - -

Verschoben nach /articles

-
- - diff --git a/apps/mana/apps/web/src/routes/(app)/news/sources/+page.svelte b/apps/mana/apps/web/src/routes/(app)/news/sources/+page.svelte deleted file mode 100644 index 7d0262ad1..000000000 --- a/apps/mana/apps/web/src/routes/(app)/news/sources/+page.svelte +++ /dev/null @@ -1,168 +0,0 @@ - - - - - Quellen — News — Mana - - - -
-
- -

Quellen

-

- {prefs.blockedSources.length} blockiert. Tippe auf eine Quelle um sie ein- oder auszublenden. -

-
- - {#each visibleTopics as topic} -
-

- {TOPIC_LABELS[topic].emoji} - {TOPIC_LABELS[topic].de} -

-
- {#each sourcesForTopic(topic) as src} - {@const blocked = isBlocked(src.slug)} - {@const weight = weightOf(src.slug)} - - {/each} -
-
- {/each} -
-
- - diff --git a/docker-compose.macmini.yml b/docker-compose.macmini.yml index 082e9a85d..3d4130ef1 100644 --- a/docker-compose.macmini.yml +++ b/docker-compose.macmini.yml @@ -1304,10 +1304,8 @@ services: MANA_CREDITS_URL: http://mana-credits:3002 MANA_MEDIA_URL: http://mana-media:3011 MANA_CRAWLER_URL: http://mana-crawler:3014 - # mana-news-pool — Plattform-Service (Lift-B 2026-05-16). Ersetzt - # den ehemaligen news-ingester:3066-Container, der direkt in - # mana_platform.news.curated_articles schrieb. apps/api/news/routes.ts - # ist seit 2026-05-17 ein HTTP-Proxy auf diesen Endpoint. + # mana-news-pool — Plattform-Service (Lift-B 2026-05-16). Wird + # vom news-research-Modul + von der Pageta-Standalone konsumiert. MANA_NEWS_POOL_URL: http://mana-news-pool:3079 MANA_LLM_DEFAULT_MODEL: ${MANA_LLM_DEFAULT_MODEL:-gemma3:4b} MANA_SERVICE_KEY: ${MANA_SERVICE_KEY} diff --git a/docker/prometheus/prometheus.yml b/docker/prometheus/prometheus.yml index 821bf931f..614a2cab4 100644 --- a/docker/prometheus/prometheus.yml +++ b/docker/prometheus/prometheus.yml @@ -251,7 +251,6 @@ scrape_configs: - https://mana.how/finance - https://mana.how/places # mana.how/who: existiert nicht im unified-app — Who läuft als Standalone-Stack auf who.mana.how - - https://mana.how/news - https://mana.how/mail - https://mana.how/playground # ─── Standalone Apps / Games (separate Container, eigene Tunnel-Hostnames) ─── diff --git a/packages/shared-ai/src/tools/schemas.ts b/packages/shared-ai/src/tools/schemas.ts index 840ee8995..1fafe0084 100644 --- a/packages/shared-ai/src/tools/schemas.ts +++ b/packages/shared-ai/src/tools/schemas.ts @@ -326,30 +326,6 @@ export const AI_TOOL_CATALOG: readonly ToolSchema[] = [ ], }, - // ── News ────────────────────────────────────────────────── - { - name: 'save_news_article', - module: 'news', - description: - 'Speichert einen Artikel von einer URL in die Leseliste. URL wird serverseitig per Readability extrahiert.', - defaultPolicy: 'propose', - parameters: [ - { name: 'url', type: 'string', description: 'Die Artikel-URL', required: true }, - { - name: 'title', - type: 'string', - description: 'Anzeigetitel für den Approval-Dialog (informativ)', - required: false, - }, - { - name: 'summary', - type: 'string', - description: 'Kurze Begründung warum dieser Artikel relevant ist', - required: false, - }, - ], - }, - // ── Articles (Pocket-style read-it-later) ─────────────── { name: 'list_articles', diff --git a/packages/shared-branding/src/app-icons.ts b/packages/shared-branding/src/app-icons.ts index e6196932a..3fbf24bac 100644 --- a/packages/shared-branding/src/app-icons.ts +++ b/packages/shared-branding/src/app-icons.ts @@ -107,9 +107,6 @@ export const APP_ICONS = { uload: svgToDataUrl( `` ), - news: svgToDataUrl( - `` - ), 'news-research': svgToDataUrl( `` ), diff --git a/packages/shared-branding/src/mana-apps.ts b/packages/shared-branding/src/mana-apps.ts index f30c0002d..635d19d05 100644 --- a/packages/shared-branding/src/mana-apps.ts +++ b/packages/shared-branding/src/mana-apps.ts @@ -413,23 +413,6 @@ export const MANA_APPS: ManaApp[] = [ status: 'beta', requiredTier: 'guest', }, - { - id: 'news', - name: 'News Hub', - description: { - de: 'Kuratierter Newsfeed', - en: 'Curated News Feed', - }, - longDescription: { - de: 'Kuratierter Newsfeed aus öffentlichen Quellen mit persönlicher Leseliste — wähle Themen aus, blende Quellen aus und bau dir deinen eigenen Feed.', - en: 'Curated news feed from public sources with a personal reading list — pick topics, hide sources, and shape your own feed.', - }, - icon: APP_ICONS.news, - color: '#10b981', - comingSoon: false, - status: 'development', - requiredTier: 'guest', - }, { id: 'news-research', name: 'News Research', diff --git a/packages/shared-types/src/spaces.ts b/packages/shared-types/src/spaces.ts index f230c7a0c..a874f9b8d 100644 --- a/packages/shared-types/src/spaces.ts +++ b/packages/shared-types/src/spaces.ts @@ -77,7 +77,6 @@ export const SPACE_MODULE_ALLOWLIST: Record