diff --git a/apps/api/src/modules/articles/import-worker.ts b/apps/api/src/modules/articles/import-worker.ts index 39bba2c14..f05ee09d7 100644 --- a/apps/api/src/modules/articles/import-worker.ts +++ b/apps/api/src/modules/articles/import-worker.ts @@ -22,6 +22,12 @@ * Plan: docs/plans/articles-bulk-import.md. */ +// Operational logs (boot, tick errors, GC summary, stale-recovery +// sweep) go to console intentionally — same pattern as +// services/mana-ai/src/cron/tick.ts. Captured by the apps/api stdout +// aggregator; structured signal lives in Prometheus counters. +/* eslint-disable no-console */ + import { getSyncConnection } from '../../lib/sync-db'; import { articlesImportJobsCompletedTotal, 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 6c245520d..04f45bfb0 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 @@ -1016,4 +1016,25 @@ export const MODULE_HELP: Record = { 'Eigene API-Keys unter "🔑 API-Keys" — überschreiben den Server-Key und kosten dich keine Credits', ], }, + articles: { + description: + 'Pocket-Style Read-it-Later — speichere Web-Artikel zum späteren Lesen. URLs werden serverseitig per Readability extrahiert, landen verschlüsselt in deiner IndexedDB und sind danach offline lesbar im Reader-View. Mit Highlights, Tags und Reading-Progress.', + features: [ + 'URL einfügen → Server extrahiert via Readability → verschlüsselt gespeichert', + 'Reader-View mit Serif/Sans-Auswahl, Light/Sepia/Dark, Schriftgröße', + 'Highlights mit 4 Farben + optionalen Notizen pro Selektion', + 'Tags aus dem globalen Tag-Pool', + 'Reading-Progress wird automatisch beim Scrollen gespeichert', + 'Bookmarklet (URL + HTML-Variante für Cookie-gewallte Seiten)', + 'Share-Target auf Android/Chromium PWA', + 'Bulk-Import: Mehrere URLs auf einmal über /articles/import — Server arbeitet im Hintergrund, Tab-Close-resistent, Multi-Device-sichtbar', + 'AI-Tools: Artikel speichern, archivieren, taggen, Highlight setzen, Bulk-Import starten', + ], + tips: [ + 'Cookie-Wand erkannt? → /articles/settings → "Browser-HTML-Bookmarklet" benutzt deine bestehende Browser-Session', + 'Mehrere URLs gleichzeitig? → /articles/import — eine pro Zeile oder durch Komma getrennt, max 200 pro Job', + 'Im Bulk-Import-Detail zeigt jede Cookie-Wand-Zeile einen "Erneut speichern"-Link der direkt zum Bookmarklet-Flow springt', + 'Teaser stellt sich raus dass der Server nur den Cookie-Banner extrahiert hat? → einfach mit dem HTML-Bookmarklet überschreiben — die Article-ID bleibt', + ], + }, }; diff --git a/apps/mana/apps/web/src/lib/i18n/locales/articles/de.json b/apps/mana/apps/web/src/lib/i18n/locales/articles/de.json index f939d911a..75c50cd14 100644 --- a/apps/mana/apps/web/src/lib/i18n/locales/articles/de.json +++ b/apps/mana/apps/web/src/lib/i18n/locales/articles/de.json @@ -34,5 +34,67 @@ "open_original": "Original-Seite öffnen", "delete_label": "Artikel löschen", "confirm_delete": "Artikel wirklich löschen?" + }, + "import": { + "bulk_link": "Mehrere URLs auf einmal? → Bulk-Import", + "form_title": "Mehrere Artikel importieren", + "form_subtitle": "Eine URL pro Zeile (oder durch Leerzeichen / Komma getrennt). Mana extrahiert sie nacheinander im Hintergrund.", + "form_placeholder": "https://example.com/article-1\nhttps://example.com/article-2\n…", + "count_valid": "{n} gültig", + "count_overlimit_suffix": " / max {max}", + "count_dup": "{n} doppelt (übersprungen)", + "count_invalid": "{n} ungültig", + "invalid_details_summary": "Ungültige Zeilen anzeigen ({n})", + "error_no_urls": "Mindestens eine gültige URL einfügen.", + "error_overlimit": "Zu viele URLs ({n}). Maximal {max} pro Job — splitte den Import.", + "error_failed": "Job konnte nicht erstellt werden.", + "submit_label": "{n} URLs importieren", + "submit_busy": "Erstelle Job…", + "hint": "Im Hintergrund — du kannst den Tab schließen und später zurückkommen. Bei 50 URLs dauert es grob 5–10 Minuten. Den Fortschritt siehst du auf der Detailseite.", + "jobs_heading": "Bisherige Imports", + "filter_all": "Alle ({n})", + "filter_active": "Aktiv ({n})", + "filter_done": "Fertig ({n})", + "filter_errors": "Mit Fehlern ({n})", + "empty_filter": "Keine Jobs in dieser Ansicht.", + "status_queued": "Wartet", + "status_running": "Läuft", + "status_paused": "Pausiert", + "status_done": "Fertig", + "status_cancelled": "Abgebrochen", + "jobs_meta_errors": "{n} Fehler", + "jobs_meta_dups": "{n} Duplikate", + "jobs_meta_warnings": "{n} Warnungen", + "detail_title": "Import-Job", + "detail_not_found": "Job nicht gefunden.", + "detail_progress_aria": "Fortschritt", + "detail_counter_total": "{done} / {total} verarbeitet", + "detail_counter_saved": "{n} gespeichert", + "detail_counter_dups": "{n} Duplikate", + "detail_counter_warns": "{n} mit Cookie-Wand", + "detail_counter_errors": "{n} Fehler", + "action_pause": "Pause", + "action_resume": "Fortsetzen", + "action_cancel": "Abbrechen", + "action_retry": "Fehler wiederholen", + "action_delete": "Löschen", + "confirm_cancel": "Job wirklich abbrechen? Bisherige Artikel bleiben gespeichert.", + "confirm_delete": "Job-Historie löschen? Artikel bleiben.", + "consent_hint_strong": "Cookie-Wand erkannt", + "consent_hint_body": "{n, plural, one {# Artikel hat} other {# Artikel haben}} nur den Cookie-Zustimmungs-Dialog gespeichert (der Server sieht keine Cookies). Lösung:", + "consent_hint_link": "Browser-HTML-Bookmarklet", + "consent_hint_after_link": "aus dem Tab in dem du dem Cookie zugestimmt hast benutzen — überschreibt den Teaser durch den echten Artikel.", + "item_pending": "Wartet", + "item_extracting": "Extrahiert…", + "item_extracted": "Server fertig", + "item_saved": "✓ Gespeichert", + "item_duplicate": "· Duplikat", + "item_consent_wall": "⚠ Cookie-Wand", + "item_error": "✗ Fehler", + "item_cancelled": "Abgebrochen", + "item_action_view_teaser": "Teaser ansehen", + "item_action_rescue": "Erneut speichern", + "item_action_rescue_tip": "Mit Bookmarklet erneut speichern — überschreibt den Teaser durch den echten Artikel", + "item_action_open": "Öffnen" } } diff --git a/apps/mana/apps/web/src/lib/i18n/locales/articles/en.json b/apps/mana/apps/web/src/lib/i18n/locales/articles/en.json index caec9225f..8d1efa16c 100644 --- a/apps/mana/apps/web/src/lib/i18n/locales/articles/en.json +++ b/apps/mana/apps/web/src/lib/i18n/locales/articles/en.json @@ -34,5 +34,67 @@ "open_original": "Open original page", "delete_label": "Delete article", "confirm_delete": "Really delete article?" + }, + "import": { + "bulk_link": "Multiple URLs at once? → Bulk import", + "form_title": "Import multiple articles", + "form_subtitle": "One URL per line (or separated by spaces / commas). Mana extracts them one after the other in the background.", + "form_placeholder": "https://example.com/article-1\nhttps://example.com/article-2\n…", + "count_valid": "{n} valid", + "count_overlimit_suffix": " / max {max}", + "count_dup": "{n} duplicate (skipped)", + "count_invalid": "{n} invalid", + "invalid_details_summary": "Show invalid lines ({n})", + "error_no_urls": "Add at least one valid URL.", + "error_overlimit": "Too many URLs ({n}). Maximum {max} per job — split the import.", + "error_failed": "Could not create job.", + "submit_label": "Import {n} URLs", + "submit_busy": "Creating job…", + "hint": "Runs in the background — you can close the tab and come back later. 50 URLs take roughly 5–10 minutes. Progress is on the detail page.", + "jobs_heading": "Past imports", + "filter_all": "All ({n})", + "filter_active": "Active ({n})", + "filter_done": "Done ({n})", + "filter_errors": "With errors ({n})", + "empty_filter": "No jobs in this view.", + "status_queued": "Queued", + "status_running": "Running", + "status_paused": "Paused", + "status_done": "Done", + "status_cancelled": "Cancelled", + "jobs_meta_errors": "{n} errors", + "jobs_meta_dups": "{n} duplicates", + "jobs_meta_warnings": "{n} warnings", + "detail_title": "Import job", + "detail_not_found": "Job not found.", + "detail_progress_aria": "Progress", + "detail_counter_total": "{done} / {total} processed", + "detail_counter_saved": "{n} saved", + "detail_counter_dups": "{n} duplicates", + "detail_counter_warns": "{n} with cookie wall", + "detail_counter_errors": "{n} errors", + "action_pause": "Pause", + "action_resume": "Resume", + "action_cancel": "Cancel", + "action_retry": "Retry errors", + "action_delete": "Delete", + "confirm_cancel": "Really cancel job? Already-saved articles stay.", + "confirm_delete": "Delete job history? Articles stay.", + "consent_hint_strong": "Cookie wall detected", + "consent_hint_body": "{n, plural, one {# article has} other {# articles have}} only saved the cookie consent dialog (the server sees no cookies). Fix:", + "consent_hint_link": "Browser HTML bookmarklet", + "consent_hint_after_link": "from the tab where you already accepted — overwrites the teaser with the real article.", + "item_pending": "Waiting", + "item_extracting": "Extracting…", + "item_extracted": "Server done", + "item_saved": "✓ Saved", + "item_duplicate": "· Duplicate", + "item_consent_wall": "⚠ Cookie wall", + "item_error": "✗ Error", + "item_cancelled": "Cancelled", + "item_action_view_teaser": "View teaser", + "item_action_rescue": "Save again", + "item_action_rescue_tip": "Save again with the bookmarklet — overwrites the teaser with the real article", + "item_action_open": "Open" } } diff --git a/apps/mana/apps/web/src/lib/i18n/locales/articles/es.json b/apps/mana/apps/web/src/lib/i18n/locales/articles/es.json index 197fbd7ab..7f935286b 100644 --- a/apps/mana/apps/web/src/lib/i18n/locales/articles/es.json +++ b/apps/mana/apps/web/src/lib/i18n/locales/articles/es.json @@ -34,5 +34,67 @@ "open_original": "Abrir página original", "delete_label": "Eliminar artículo", "confirm_delete": "¿Eliminar realmente el artículo?" + }, + "import": { + "bulk_link": "¿Varias URLs a la vez? → Importación masiva", + "form_title": "Importar varios artículos", + "form_subtitle": "Una URL por línea (o separadas por espacios / comas). Mana las extrae una tras otra en segundo plano.", + "form_placeholder": "https://example.com/article-1\nhttps://example.com/article-2\n…", + "count_valid": "{n} válidas", + "count_overlimit_suffix": " / máx {max}", + "count_dup": "{n} duplicadas (omitidas)", + "count_invalid": "{n} inválidas", + "invalid_details_summary": "Mostrar líneas inválidas ({n})", + "error_no_urls": "Añade al menos una URL válida.", + "error_overlimit": "Demasiadas URLs ({n}). Máximo {max} por job — divide la importación.", + "error_failed": "No se pudo crear el job.", + "submit_label": "Importar {n} URLs", + "submit_busy": "Creando job…", + "hint": "Se ejecuta en segundo plano — puedes cerrar la pestaña y volver más tarde. 50 URLs tardan unos 5–10 minutos. El progreso está en la página de detalle.", + "jobs_heading": "Importaciones anteriores", + "filter_all": "Todos ({n})", + "filter_active": "Activos ({n})", + "filter_done": "Completados ({n})", + "filter_errors": "Con errores ({n})", + "empty_filter": "Ningún job en esta vista.", + "status_queued": "En cola", + "status_running": "En ejecución", + "status_paused": "Pausado", + "status_done": "Completado", + "status_cancelled": "Cancelado", + "jobs_meta_errors": "{n} errores", + "jobs_meta_dups": "{n} duplicados", + "jobs_meta_warnings": "{n} advertencias", + "detail_title": "Job de importación", + "detail_not_found": "Job no encontrado.", + "detail_progress_aria": "Progreso", + "detail_counter_total": "{done} / {total} procesados", + "detail_counter_saved": "{n} guardados", + "detail_counter_dups": "{n} duplicados", + "detail_counter_warns": "{n} con muro de cookies", + "detail_counter_errors": "{n} errores", + "action_pause": "Pausar", + "action_resume": "Reanudar", + "action_cancel": "Cancelar", + "action_retry": "Reintentar errores", + "action_delete": "Eliminar", + "confirm_cancel": "¿Cancelar realmente el job? Los artículos ya guardados permanecen.", + "confirm_delete": "¿Eliminar el historial del job? Los artículos permanecen.", + "consent_hint_strong": "Muro de cookies detectado", + "consent_hint_body": "{n, plural, one {# artículo solo ha} other {# artículos solo han}} guardado el diálogo de consentimiento de cookies (el servidor no ve cookies). Solución:", + "consent_hint_link": "bookmarklet HTML del navegador", + "consent_hint_after_link": "desde la pestaña donde ya aceptaste — sobrescribe el teaser con el artículo real.", + "item_pending": "En espera", + "item_extracting": "Extrayendo…", + "item_extracted": "Servidor listo", + "item_saved": "✓ Guardado", + "item_duplicate": "· Duplicado", + "item_consent_wall": "⚠ Muro de cookies", + "item_error": "✗ Error", + "item_cancelled": "Cancelado", + "item_action_view_teaser": "Ver teaser", + "item_action_rescue": "Guardar de nuevo", + "item_action_rescue_tip": "Guardar de nuevo con el bookmarklet — sobrescribe el teaser con el artículo real", + "item_action_open": "Abrir" } } diff --git a/apps/mana/apps/web/src/lib/i18n/locales/articles/fr.json b/apps/mana/apps/web/src/lib/i18n/locales/articles/fr.json index e42930ba4..f881222cb 100644 --- a/apps/mana/apps/web/src/lib/i18n/locales/articles/fr.json +++ b/apps/mana/apps/web/src/lib/i18n/locales/articles/fr.json @@ -34,5 +34,67 @@ "open_original": "Ouvrir la page d'origine", "delete_label": "Supprimer l'article", "confirm_delete": "Vraiment supprimer l'article ?" + }, + "import": { + "bulk_link": "Plusieurs URLs à la fois ? → Import groupé", + "form_title": "Importer plusieurs articles", + "form_subtitle": "Une URL par ligne (ou séparées par des espaces / virgules). Mana les extrait l'une après l'autre en arrière-plan.", + "form_placeholder": "https://example.com/article-1\nhttps://example.com/article-2\n…", + "count_valid": "{n} valides", + "count_overlimit_suffix": " / max {max}", + "count_dup": "{n} doublons (ignorés)", + "count_invalid": "{n} invalides", + "invalid_details_summary": "Afficher les lignes invalides ({n})", + "error_no_urls": "Ajoute au moins une URL valide.", + "error_overlimit": "Trop d'URLs ({n}). Maximum {max} par job — divise l'import.", + "error_failed": "Impossible de créer le job.", + "submit_label": "Importer {n} URLs", + "submit_busy": "Création du job…", + "hint": "S'exécute en arrière-plan — tu peux fermer l'onglet et revenir plus tard. 50 URLs prennent environ 5–10 minutes. La progression est visible sur la page de détail.", + "jobs_heading": "Imports passés", + "filter_all": "Tous ({n})", + "filter_active": "Actifs ({n})", + "filter_done": "Terminés ({n})", + "filter_errors": "Avec erreurs ({n})", + "empty_filter": "Aucun job dans cette vue.", + "status_queued": "En attente", + "status_running": "En cours", + "status_paused": "En pause", + "status_done": "Terminé", + "status_cancelled": "Annulé", + "jobs_meta_errors": "{n} erreurs", + "jobs_meta_dups": "{n} doublons", + "jobs_meta_warnings": "{n} avertissements", + "detail_title": "Job d'import", + "detail_not_found": "Job introuvable.", + "detail_progress_aria": "Progression", + "detail_counter_total": "{done} / {total} traités", + "detail_counter_saved": "{n} enregistrés", + "detail_counter_dups": "{n} doublons", + "detail_counter_warns": "{n} avec mur de cookies", + "detail_counter_errors": "{n} erreurs", + "action_pause": "Pause", + "action_resume": "Reprendre", + "action_cancel": "Annuler", + "action_retry": "Réessayer les erreurs", + "action_delete": "Supprimer", + "confirm_cancel": "Vraiment annuler le job ? Les articles déjà enregistrés restent.", + "confirm_delete": "Supprimer l'historique du job ? Les articles restent.", + "consent_hint_strong": "Mur de cookies détecté", + "consent_hint_body": "{n, plural, one {# article n'a enregistré} other {# articles n'ont enregistré}} que la boîte de dialogue de consentement (le serveur ne voit aucun cookie). Solution :", + "consent_hint_link": "bookmarklet HTML du navigateur", + "consent_hint_after_link": "depuis l'onglet où tu as déjà accepté — remplace le teaser par l'article réel.", + "item_pending": "En attente", + "item_extracting": "Extraction…", + "item_extracted": "Serveur terminé", + "item_saved": "✓ Enregistré", + "item_duplicate": "· Doublon", + "item_consent_wall": "⚠ Mur de cookies", + "item_error": "✗ Erreur", + "item_cancelled": "Annulé", + "item_action_view_teaser": "Voir le teaser", + "item_action_rescue": "Réenregistrer", + "item_action_rescue_tip": "Réenregistrer avec le bookmarklet — remplace le teaser par l'article réel", + "item_action_open": "Ouvrir" } } diff --git a/apps/mana/apps/web/src/lib/i18n/locales/articles/it.json b/apps/mana/apps/web/src/lib/i18n/locales/articles/it.json index 855914755..4fb13c839 100644 --- a/apps/mana/apps/web/src/lib/i18n/locales/articles/it.json +++ b/apps/mana/apps/web/src/lib/i18n/locales/articles/it.json @@ -34,5 +34,67 @@ "open_original": "Apri pagina originale", "delete_label": "Elimina articolo", "confirm_delete": "Eliminare davvero l'articolo?" + }, + "import": { + "bulk_link": "Più URL contemporaneamente? → Import multiplo", + "form_title": "Importa più articoli", + "form_subtitle": "Una URL per riga (o separate da spazi / virgole). Mana le estrae una dopo l'altra in background.", + "form_placeholder": "https://example.com/article-1\nhttps://example.com/article-2\n…", + "count_valid": "{n} valide", + "count_overlimit_suffix": " / max {max}", + "count_dup": "{n} duplicate (ignorate)", + "count_invalid": "{n} non valide", + "invalid_details_summary": "Mostra righe non valide ({n})", + "error_no_urls": "Aggiungi almeno una URL valida.", + "error_overlimit": "Troppe URL ({n}). Massimo {max} per job — dividi l'import.", + "error_failed": "Impossibile creare il job.", + "submit_label": "Importa {n} URL", + "submit_busy": "Creazione del job…", + "hint": "Funziona in background — puoi chiudere la scheda e tornare più tardi. 50 URL richiedono circa 5–10 minuti. Lo stato di avanzamento è nella pagina di dettaglio.", + "jobs_heading": "Import precedenti", + "filter_all": "Tutti ({n})", + "filter_active": "Attivi ({n})", + "filter_done": "Completati ({n})", + "filter_errors": "Con errori ({n})", + "empty_filter": "Nessun job in questa vista.", + "status_queued": "In attesa", + "status_running": "In esecuzione", + "status_paused": "In pausa", + "status_done": "Completato", + "status_cancelled": "Annullato", + "jobs_meta_errors": "{n} errori", + "jobs_meta_dups": "{n} duplicati", + "jobs_meta_warnings": "{n} avvisi", + "detail_title": "Job di import", + "detail_not_found": "Job non trovato.", + "detail_progress_aria": "Avanzamento", + "detail_counter_total": "{done} / {total} elaborati", + "detail_counter_saved": "{n} salvati", + "detail_counter_dups": "{n} duplicati", + "detail_counter_warns": "{n} con cookie wall", + "detail_counter_errors": "{n} errori", + "action_pause": "Pausa", + "action_resume": "Riprendi", + "action_cancel": "Annulla", + "action_retry": "Riprova errori", + "action_delete": "Elimina", + "confirm_cancel": "Annullare davvero il job? Gli articoli già salvati rimangono.", + "confirm_delete": "Eliminare la cronologia del job? Gli articoli rimangono.", + "consent_hint_strong": "Cookie wall rilevato", + "consent_hint_body": "{n, plural, one {# articolo ha} other {# articoli hanno}} salvato solo la finestra di consenso ai cookie (il server non vede cookie). Soluzione:", + "consent_hint_link": "bookmarklet HTML del browser", + "consent_hint_after_link": "dalla scheda dove hai già accettato — sovrascrive il teaser con l'articolo reale.", + "item_pending": "In attesa", + "item_extracting": "Estrazione…", + "item_extracted": "Server pronto", + "item_saved": "✓ Salvato", + "item_duplicate": "· Duplicato", + "item_consent_wall": "⚠ Cookie wall", + "item_error": "✗ Errore", + "item_cancelled": "Annullato", + "item_action_view_teaser": "Vedi teaser", + "item_action_rescue": "Salva di nuovo", + "item_action_rescue_tip": "Salva di nuovo con il bookmarklet — sovrascrive il teaser con l'articolo reale", + "item_action_open": "Apri" } } diff --git a/apps/mana/apps/web/src/lib/modules/articles/components/AddUrlForm.svelte b/apps/mana/apps/web/src/lib/modules/articles/components/AddUrlForm.svelte index d46cce29b..eb62790bb 100644 --- a/apps/mana/apps/web/src/lib/modules/articles/components/AddUrlForm.svelte +++ b/apps/mana/apps/web/src/lib/modules/articles/components/AddUrlForm.svelte @@ -18,6 +18,7 @@ import { goto } from '$app/navigation'; import { page } from '$app/stores'; import { onMount, onDestroy } from 'svelte'; + import { _ } from 'svelte-i18n'; import { articlesStore } from '../stores/articles.svelte'; import { extractArticle, extractFromHtml, type ExtractedArticle } from '../api'; import type { Article } from '../types'; @@ -258,7 +259,7 @@ {#if (loading || saving) && !error && !preview && !duplicate} diff --git a/apps/mana/apps/web/src/lib/modules/articles/components/BulkImportForm.svelte b/apps/mana/apps/web/src/lib/modules/articles/components/BulkImportForm.svelte index 848b33d94..43c3dc0a6 100644 --- a/apps/mana/apps/web/src/lib/modules/articles/components/BulkImportForm.svelte +++ b/apps/mana/apps/web/src/lib/modules/articles/components/BulkImportForm.svelte @@ -7,6 +7,7 @@ --> @@ -58,7 +59,7 @@ {#if allJobs.length > 0}
-

Bisherige Imports

+

{$_('articles.import.jobs_heading')}

{#if visibleJobs.length === 0} -

Keine Jobs in dieser Ansicht.

+

{$_('articles.import.empty_filter')}

{/if}