diff --git a/backend/services/dashboardService.js b/backend/services/dashboardService.js index 267bdd4..b3277ec 100644 --- a/backend/services/dashboardService.js +++ b/backend/services/dashboardService.js @@ -45,10 +45,13 @@ class DashboardService extends BaseService { title: String(w?.title ?? ''), endpoint: String(w?.endpoint ?? '') })).filter(w => w.id && (w.title || w.endpoint)); - await UserDashboard.upsert({ - userId: user.id, - config: { widgets: sanitized } - }, { conflictFields: ['userId'] }); + const payload = { widgets: sanitized }; + const existing = await UserDashboard.findOne({ where: { userId: user.id } }); + if (existing) { + await existing.update({ config: payload }); + } else { + await UserDashboard.create({ userId: user.id, config: payload }); + } return { widgets: sanitized }; } } diff --git a/backend/services/newsService.js b/backend/services/newsService.js index 2cc1353..36ad4f6 100644 --- a/backend/services/newsService.js +++ b/backend/services/newsService.js @@ -38,33 +38,34 @@ async function fetchNewsPage({ counter, language = 'de', category = 'top', nextP } /** - * Liefert die Seite für das „counter.“-te Widget (0 = erste Seite, 1 = zweite, …). - * Ruft die API ggf. mehrfach auf (nextPage), damit jede Widget-Instanz andere Artikel bekommt. + * Liefert den N-ten Artikel (counter = 0, 1, 2, …) für das N-te News-Widget. + * Lädt ggf. mehrere Seiten, bis genug Artikel vorhanden sind; jede Widget-Instanz bekommt einen anderen Artikel. * * @param {object} options - * @param {number} options.counter + * @param {number} options.counter - Index des Artikels (0 = erster, 1 = zweiter, …) * @param {string} [options.language] * @param {string} [options.category] * @returns {Promise<{ results: Array, nextPage: string|null }>} */ async function getNews({ counter = 0, language = 'de', category = 'top' }) { + const neededIndex = Math.max(0, counter); + const collected = []; let nextPageToken = null; - let lastResult = { results: [], nextPage: null }; - for (let i = 0; i <= counter; i++) { - lastResult = await fetchNewsPage({ - counter: i, + while (collected.length <= neededIndex) { + const page = await fetchNewsPage({ language, category, nextPageToken: nextPageToken || undefined }); - nextPageToken = lastResult.nextPage; - if (!nextPageToken && i < counter) { - break; - } + const items = page.results ?? []; + collected.push(...items); + nextPageToken = page.nextPage ?? null; + if (items.length === 0 || !nextPageToken) break; } - return lastResult; + const single = collected[neededIndex] ? [collected[neededIndex]] : []; + return { results: single, nextPage: null }; } export default { getNews }; diff --git a/backend/utils/syncDatabase.js b/backend/utils/syncDatabase.js index 66c0780..3a3dfd3 100644 --- a/backend/utils/syncDatabase.js +++ b/backend/utils/syncDatabase.js @@ -108,6 +108,23 @@ const syncDatabase = async () => { console.warn('⚠️ Konnte type.widget_type nicht anlegen:', e?.message || e); } + // Dashboard: Benutzer-Konfiguration (Widgets pro User) + console.log("Ensuring user_dashboard table exists..."); + try { + await sequelize.query(` + CREATE TABLE IF NOT EXISTS community.user_dashboard ( + user_id INTEGER NOT NULL PRIMARY KEY, + config JSONB NOT NULL DEFAULT '{"widgets":[]}'::jsonb, + CONSTRAINT user_dashboard_user_fk + FOREIGN KEY (user_id) + REFERENCES community."user"(id) + ON DELETE CASCADE + ); + `); + } catch (e) { + console.warn('⚠️ Konnte community.user_dashboard nicht anlegen:', e?.message || e); + } + // Vokabeltrainer: Tabellen sicherstellen (auch ohne manuell ausgeführte Migrations) // Hintergrund: In Produktion sind Schema-Updates deaktiviert, und Migrations werden nicht automatisch ausgeführt. // Damit API/Menu nicht mit "relation does not exist" (42P01) scheitert, legen wir die Tabellen idempotent an. diff --git a/frontend/src/views/home/LoggedInView.vue b/frontend/src/views/home/LoggedInView.vue index 152f06e..5898d4a 100644 --- a/frontend/src/views/home/LoggedInView.vue +++ b/frontend/src/views/home/LoggedInView.vue @@ -28,6 +28,14 @@ {{ wt.label }} +