feat: Phase 4 — Wetter-Widget, Wiederkehrende Aufgaben, Kanban-Ansicht, PWA

- server/routes/weather.js: OpenWeatherMap-Proxy (aktuelles Wetter + 3-Tage-Forecast,
  30-min-Cache, graceful fallback wenn kein API-Key gesetzt)
- public/pages/dashboard.js: Weather-Widget parallel mit Dashboard-Daten laden
- public/styles/dashboard.css: Weather-Widget-Styles (Gradient, Forecast-Strip)
- server/services/recurrence.js: RRULE-Parser (FREQ=DAILY/WEEKLY/MONTHLY, BYDAY,
  INTERVAL, UNTIL) + nextOccurrence()-Funktion
- server/routes/tasks.js: Bei PATCH /:id/status = done → nächste Instanz
  wiederkehrender Aufgaben automatisch anlegen
- public/pages/tasks.js: Kanban-Ansicht (3 Spalten: Offen/In Bearbeitung/Erledigt)
  mit HTML5 Drag & Drop, View-Toggle (Liste/Kanban)
- public/styles/tasks.css: Kanban-Board-Styles (Spalten, Cards, Drag-over-Highlight)
- public/sw.js: Cache-Version auf v2, alle Modul-CSS-Dateien im APP_SHELL-Cache

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ulsklyc
2026-03-24 21:32:22 +01:00
parent 74b6e5f078
commit 450ae37f42
8 changed files with 669 additions and 26 deletions
+58 -3
View File
@@ -228,6 +228,54 @@ function renderPinnedNotes(notes) {
return `<div class="widget">${header}<div class="widget__body">${items}</div></div>`;
}
// --------------------------------------------------------
// Wetter-Widget
// --------------------------------------------------------
const WEATHER_ICON_BASE = 'https://openweathermap.org/img/wn/';
function weatherIconUrl(icon) {
return `${WEATHER_ICON_BASE}${icon}@2x.png`;
}
function renderWeatherWidget(weather) {
if (!weather) return ''; // Kein API-Key → Widget ausblenden
const { city, current, forecast } = weather;
const forecastHtml = forecast.map((d) => {
const date = new Date(d.date + 'T12:00:00');
const label = date.toLocaleDateString('de-DE', { weekday: 'short' });
return `
<div class="weather-forecast__day">
<div class="weather-forecast__label">${label}</div>
<img class="weather-forecast__icon" src="${weatherIconUrl(d.icon)}"
alt="${d.desc}" width="32" height="32" loading="lazy">
<div class="weather-forecast__temps">
<span class="weather-forecast__high">${d.temp_max}°</span>
<span class="weather-forecast__low">${d.temp_min}°</span>
</div>
</div>`;
}).join('');
return `
<div class="widget weather-widget">
<div class="weather-widget__main">
<div class="weather-widget__left">
<div class="weather-widget__temp">${current.temp}°C</div>
<div class="weather-widget__desc">${current.desc}</div>
<div class="weather-widget__city">${city}</div>
<div class="weather-widget__meta">
Gefühlt ${current.feels_like}° · ${current.humidity}% Luftfeuchtigkeit · Wind ${current.wind_speed} km/h
</div>
</div>
<img class="weather-widget__icon" src="${weatherIconUrl(current.icon)}"
alt="${current.desc}" width="80" height="80" loading="lazy">
</div>
${forecast.length ? `<div class="weather-forecast">${forecastHtml}</div>` : ''}
</div>`;
}
// --------------------------------------------------------
// FAB Speed-Dial
// --------------------------------------------------------
@@ -336,10 +384,16 @@ export async function render(container, { user }) {
`;
initFab(container);
// Daten laden
let data = { upcomingEvents: [], urgentTasks: [], todayMeals: [], pinnedNotes: [] };
// Daten laden (Dashboard + Wetter parallel)
let data = { upcomingEvents: [], urgentTasks: [], todayMeals: [], pinnedNotes: [] };
let weather = null;
try {
data = await api.get('/dashboard');
const [dashRes, weatherRes] = await Promise.all([
api.get('/dashboard'),
api.get('/weather').catch(() => ({ data: null })),
]);
data = dashRes;
weather = weatherRes.data ?? null;
} catch (err) {
console.error('[Dashboard] Ladefehler:', err.message);
window.oikos?.showToast('Dashboard konnte nicht vollständig geladen werden.', 'warning');
@@ -350,6 +404,7 @@ export async function render(container, { user }) {
<div class="dashboard">
<div class="dashboard__grid">
${renderGreeting(user)}
${renderWeatherWidget(weather)}
${renderUrgentTasks(data.urgentTasks ?? [])}
${renderUpcomingEvents(data.upcomingEvents ?? [])}
${renderTodayMeals(data.todayMeals ?? [])}