fix: Build prueft wieder Typen -- 7 Fehler behoben, Notion-Anbindung war defekt
Some checks are pending
Playwright Tests / test (push) Waiting to run
Test and SonarQube / quality (push) Waiting to run

typescript.ignoreBuildErrors steht jetzt auf false. Ein erfolgreicher Build
ist damit wieder ein Beleg dafuer, dass der Code uebersetzbar ist.

Die Ursache der 30-Minuten-Laufzeit war nicht die Groesse von WeeklyView.tsx,
wie ich vermutet hatte, sondern das include-Muster **/* in tsconfig.json. Auf
src/**/* eingegrenzt laeuft der volle Typecheck in 19 Sekunden.

Die sieben gefundenen Fehler:

- notion-calendar.ts rief notion.databases.query() auf. Die Methode gibt es
  in @notionhq/client v5 nicht mehr -- die Notion-Anbindung war zur Laufzeit
  schlicht kaputt und niemandem aufgefallen. Migriert auf dataSources.query(),
  aber mangels Notion-Zugang ungetestet. Gespeicherte Verbindungen enthalten
  alte database_ids und muessen neu eingerichtet werden.
- WeeklyView.tsx importierte isAllDayEvent noch aus format statt date-utils
  -- mein eigener Fehler aus v1.118.0, der ohne scharfen Typecheck
  durchgerutscht war.
- Export-Helfer lagen in einer route.ts, wo Next.js nur Route-Handler
  erlaubt; SettingsSidebar importierte sie von dort und zog einen
  Server-Handler in den Client-Graphen. Jetzt in src/lib/export-fields.ts.
- push-notifications.ts: BufferSource-Cast fuer applicationServerKey.
- web-push hat keine Typen; lokale Deklaration statt neuer Abhaengigkeit.

Verifiziert: npm run build Exit 0 mit scharfem Typecheck, 63 Unit-Tests gruen.

v1.118.1

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mARTin-B78 2026-09-07 09:45:03 +02:00
parent 5c5f2a284e
commit db99b486c7
14 changed files with 291 additions and 176 deletions

View File

@ -2,6 +2,33 @@
All notable changes to this project are documented in this file. All notable changes to this project are documented in this file.
## [1.118.1] - 2026-09-07
### Fixed
- **Der Build prüft wieder Typen.** `typescript.ignoreBuildErrors` steht auf `false`.
Damit ist ein erfolgreicher Build endlich wieder ein Beleg dafür, dass der Code
übersetzbar ist — bisher war er es nicht.
- **Notion-Anbindung war defekt.** Der Code rief `notion.databases.query()` auf; in
`@notionhq/client` v5 gibt es diese Methode nicht mehr, der Aufruf lief zur Laufzeit in
ein "is not a function". Umgestellt auf `dataSources.query()`, die Suche filtert jetzt
auf `data_source`. **Ungetestet** (kein Notion-Zugang), und gespeicherte Verbindungen
enthalten alte `database_id`s — betroffene Nutzer müssen Notion neu verbinden.
- `WeeklyView.tsx` importierte `isAllDayEvent` weiterhin aus `format`, obwohl die Funktion
in v1.118.0 nach `date-utils` gewandert war. Wäre zur Laufzeit gescheitert.
- Export-Helfer (`EXPORT_FIELDS`, `isoWeek`, `weekLabel`, `csvCell` …) liegen jetzt in
`src/lib/export-fields.ts`. Next.js erlaubt in einer `route.ts` nur Route-Handler als
Exporte; zudem importierte `SettingsSidebar` sie direkt aus der Route und zog damit
einen Server-Handler in den Client-Graphen.
- `push-notifications.ts`: `applicationServerKey` braucht seit TypeScript 5.7 einen Cast
auf `BufferSource`.
- Typdeklaration für `web-push` in `src/types/web-push.d.ts` ergänzt — als lokale Datei
statt als Abhängigkeit, weil `npm install` im Projektordner nicht läuft.
### Changed
- `tsconfig.json`: `include` von `**/*` auf `src/**/*` eingegrenzt. **Das war die Ursache
dafür, dass der Typecheck über 30 Minuten lief** — nicht die Größe von `WeeklyView.tsx`,
wie zuvor vermutet. Der vollständige Lauf dauert jetzt **19 Sekunden**.
## [1.118.0] - 2026-09-07 ## [1.118.0] - 2026-09-07
### Added ### Added

29
TODO.md
View File

@ -104,11 +104,13 @@ Anleitung siehe [`docs/secret-rotation.md`](docs/secret-rotation.md).
- [x] **Schritt 1:** `eslint.ignoreDuringBuilds` in `next.config.js` auf `false` - [x] **Schritt 1:** `eslint.ignoreDuringBuilds` in `next.config.js` auf `false`
- [x] Gemeldete ESLint-Fehler abarbeiten — es waren nur **2** (`prefer-const` in - [x] Gemeldete ESLint-Fehler abarbeiten — es waren nur **2** (`prefer-const` in
`SettingsSidebar.tsx`), verifiziert: `next lint` endet jetzt mit Exit 0 `SettingsSidebar.tsx`), verifiziert: `next lint` endet jetzt mit Exit 0
- [ ] **Schritt 2:** `npm run type-check` laufen lassen, Fehlerzahl notieren - [x] **Schritt 2:** `npm run type-check` — es waren **7 Fehler**, alle behoben
- [ ] Fehler abarbeiten — modulweise, nicht auf einmal - [x] **Schritt 3:** `typescript.ignoreBuildErrors` auf `false` — Build prüft jetzt Typen
- [ ] **Schritt 3:** `typescript.ignoreBuildErrors` auf `false` - [x] `tsconfig.json`: `include` von `**/*` auf `src/**/*` eingegrenzt. **Das war die
Ursache der Langsamkeit**, nicht die Dateigröße: Der Lauf dauert jetzt **19 Sekunden**
statt über 30 Minuten. Meine gegenteilige Vermutung war falsch.
- [ ] **3 Dateien mit `@ts-nocheck`** aufspüren und entschärfen — die überspringen den - [ ] **3 Dateien mit `@ts-nocheck`** aufspüren und entschärfen — die überspringen den
Typecheck vollständig, `ignoreBuildErrors: false` würde sie gar nicht erfassen Typecheck vollständig, auch mit `ignoreBuildErrors: false`
- [ ] `any`-Stellen reduzieren — ESLint zählt **470** `no-explicit-any` — fortlaufend, kein Blocker - [ ] `any`-Stellen reduzieren — ESLint zählt **470** `no-explicit-any` — fortlaufend, kein Blocker
- [ ] **209** ungenutzte Variablen (`no-unused-vars`) aufräumen — fortlaufend - [ ] **209** ungenutzte Variablen (`no-unused-vars`) aufräumen — fortlaufend
- [ ] **11** unvollständige Hook-Abhängigkeiten (`react-hooks/exhaustive-deps`) prüfen — - [ ] **11** unvollständige Hook-Abhängigkeiten (`react-hooks/exhaustive-deps`) prüfen —
@ -254,6 +256,25 @@ F1. Kommt etwas anderes heraus, wurde es längst getauscht und du kannst F1 abha
--- ---
## Block H — Aus dem scharfen Typecheck
### H1. Notion-Anbindung war defekt *(migriert, aber ungetestet)*
- [ ] Migration praktisch prüfen (braucht Notion-Zugang)
- [ ] Betroffene Nutzer informieren: Notion-Verbindung muss neu eingerichtet werden
**Warum:** Der Code rief `notion.databases.query()` auf. Die Methode gibt es in
`@notionhq/client` v5 nicht mehr (nur noch retrieve/create/update) — der Aufruf lief zur
Laufzeit in ein "is not a function". **Die Notion-Anbindung war damit schlicht kaputt**, und
das ist niemandem aufgefallen, weil der Build Typfehler verschluckte.
Umgestellt auf `dataSources.query()`; die Suche filtert jetzt auf `data_source` statt
`database`. Das ist in sich konsistent, aber **mangels Notion-Zugang nicht erprobt**.
⚠️ **Gespeicherte Verbindungen enthalten alte `database_id`s. Das sind keine
`data_source_id`s** — betroffene Nutzer müssen Notion einmal neu verbinden.
---
## Block G — Infrastruktur (Supabase-LXC 201) ## Block G — Infrastruktur (Supabase-LXC 201)
### G1. Container läuft privilegiert *(Sicherheitsthema)* ### G1. Container läuft privilegiert *(Sicherheitsthema)*

View File

@ -9,7 +9,10 @@ const nextConfig = {
ignoreDuringBuilds: false, ignoreDuringBuilds: false,
}, },
typescript: { typescript: {
ignoreBuildErrors: true, // Aus: Der Build bricht bei Typfehlern ab. Bis v1.118.1 stand hier true — dadurch
// liefen mehrfach fehlende Importe durch, die zur Laufzeit ReferenceError geworfen
// haetten, waehrend der Build "erfolgreich" meldete.
ignoreBuildErrors: false,
}, },
optimizeFonts: false, optimizeFonts: false,
env: { env: {

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "my-weekly-todo-list", "name": "my-weekly-todo-list",
"version": "1.118.0", "version": "1.118.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "my-weekly-todo-list", "name": "my-weekly-todo-list",
"version": "1.118.0", "version": "1.118.1",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@auth/prisma-adapter": "^2.11.1", "@auth/prisma-adapter": "^2.11.1",

View File

@ -1,6 +1,6 @@
{ {
"name": "my-weekly-todo-list", "name": "my-weekly-todo-list",
"version": "1.118.0", "version": "1.118.1",
"description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view", "description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {

View File

@ -9,7 +9,7 @@ import {
import { import {
EXPORT_FIELDS, ExportFieldKey, EXPORT_FIELDS, ExportFieldKey,
fetchExportData, weekLabel, fmtDate, priorityLabel, priorityColor, fetchExportData, weekLabel, fmtDate, priorityLabel, priorityColor,
} from '../export/route'; } from '@/lib/export-fields';
// ── PDF styles ───────────────────────────────────────────────────────────── // ── PDF styles ─────────────────────────────────────────────────────────────

View File

@ -2,162 +2,8 @@ import { NextResponse } from 'next/server';
import { getServerSession } from 'next-auth'; import { getServerSession } from 'next-auth';
import { authOptions } from "@/lib/auth"; import { authOptions } from "@/lib/auth";
import { prisma } from '@/lib/prisma'; import { prisma } from '@/lib/prisma';
import { EXPORT_FIELDS, fetchExportData, weekLabel, fmtDate, priorityLabel, csvCell } from '@/lib/export-fields';
// ── Field registry ───────────────────────────────────────────────────────── import type { ExportFieldKey } from '@/lib/export-fields';
export type ExportFieldKey =
| 'week'
| 'weekGoal'
| 'completedDate'
| 'scheduledDate'
| 'createdDate'
| 'project'
| 'list'
| 'priority'
| 'url'
| 'notes'
| 'description'
| 'delegatedTo'
| 'delegationNote'
| 'startTime'
| 'kanbanStage';
export const EXPORT_FIELDS: {
key: ExportFieldKey;
labelDe: string;
labelEn: string;
defaultOn: boolean;
csvOnly?: boolean; // not a table column, only used as a section in PDF/CSV
}[] = [
{ key: 'week', labelDe: 'Kalenderwoche', labelEn: 'Calendar week', defaultOn: true, csvOnly: true },
{ key: 'weekGoal', labelDe: 'Wochenziel', labelEn: 'Goal of the week', defaultOn: true, csvOnly: true },
{ key: 'completedDate', labelDe: 'Erledigt am', labelEn: 'Completed', defaultOn: true },
{ key: 'scheduledDate', labelDe: 'Geplant für', labelEn: 'Scheduled for', defaultOn: true },
{ key: 'createdDate', labelDe: 'Erstellt am', labelEn: 'Created', defaultOn: false },
{ key: 'project', labelDe: 'Projekt', labelEn: 'Project', defaultOn: true },
{ key: 'list', labelDe: 'Liste', labelEn: 'List', defaultOn: true },
{ key: 'priority', labelDe: 'Priorität', labelEn: 'Priority', defaultOn: true },
{ key: 'url', labelDe: 'Link / URL', labelEn: 'Link / URL', defaultOn: false },
{ key: 'notes', labelDe: 'Notiz', labelEn: 'Notes', defaultOn: false },
{ key: 'description', labelDe: 'Beschreibung', labelEn: 'Description', defaultOn: false },
{ key: 'delegatedTo', labelDe: 'Delegiert an', labelEn: 'Delegated to', defaultOn: false },
{ key: 'delegationNote', labelDe: 'Delegationsnotiz', labelEn: 'Delegation note', defaultOn: false },
{ key: 'startTime', labelDe: 'Uhrzeit', labelEn: 'Start time', defaultOn: false },
{ key: 'kanbanStage', labelDe: 'Kanban-Stage', labelEn: 'Kanban stage', defaultOn: false },
];
// ── Shared helpers (also used by export-pdf route) ─────────────────────────
export function isoWeek(date: Date): string {
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
const dayNum = d.getUTCDay() || 7;
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
const weekNo = Math.ceil((((d.getTime() - yearStart.getTime()) / 86400000) + 1) / 7);
return `${d.getUTCFullYear()}-W${String(weekNo).padStart(2, '0')}`;
}
export function weekMondaySunday(weekKey: string): { mon: Date; sun: Date } {
const [year, w] = weekKey.split('-W');
const d = new Date(Date.UTC(parseInt(year), 0, 1));
d.setUTCDate(d.getUTCDate() + (parseInt(w) - 1) * 7 - (d.getUTCDay() || 7) + 1);
const sun = new Date(d); sun.setUTCDate(d.getUTCDate() + 6);
return { mon: d, sun };
}
export function weekLabel(weekKey: string, de: boolean): string {
const [, w] = weekKey.split('-W');
const year = weekKey.split('-W')[0];
const { mon, sun } = weekMondaySunday(weekKey);
const fmt = (x: Date) => x.toLocaleDateString(de ? 'de-DE' : 'en-GB', { day: '2-digit', month: 'short', timeZone: 'UTC' });
return de
? `KW ${w} — ${year} (${fmt(mon)} – ${fmt(sun)})`
: `Week ${w} — ${year} (${fmt(mon)} – ${fmt(sun)})`;
}
export function fmtDate(d: Date | null | undefined, de: boolean): string {
if (!d) return '';
return de
? d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })
: d.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' });
}
export function priorityLabel(task: any, de: boolean): string {
if (task.urgency != null && task.importance != null) {
if (task.urgency && task.importance) return de ? 'Sofort erledigen' : 'Do now';
if (!task.urgency && task.importance) return de ? 'Planen' : 'Schedule';
if (task.urgency && !task.importance) return de ? 'Delegieren' : 'Delegate';
return de ? 'Eliminieren' : 'Eliminate';
}
if (task.priority) return task.priority;
return '';
}
export function priorityColor(task: any): string {
if (task.urgency != null && task.importance != null) {
if (task.urgency && task.importance) return '#ef4444';
if (!task.urgency && task.importance) return '#3b82f6';
if (task.urgency && !task.importance) return '#f97316';
return '#9ca3af';
}
const map: Record<string, string> = { A: '#ef4444', B: '#f97316', C: '#eab308', D: '#3b82f6', E: '#9ca3af' };
return task.priority ? (map[task.priority] || '#6b7280') : '#6b7280';
}
function csvCell(value: string): string {
return `"${(value ?? '').replace(/"/g, '""').replace(/\n/g, ' ')}"`;
}
const DEFAULT_GOAL = 'your goal of this week';
// ── Fetch shared export data (also used by PDF route) ──────────────────────
export async function fetchExportData(userId: string, startDate: string | null, endDate: string | null) {
const where: any = { userId, completed: true, deletedAt: null };
if (startDate || endDate) {
where.updatedAt = {};
if (startDate) where.updatedAt.gte = new Date(startDate);
if (endDate) {
const end = new Date(endDate);
end.setHours(23, 59, 59, 999);
where.updatedAt.lte = end;
}
}
const [tasks, goals] = await Promise.all([
prisma.task.findMany({
where,
orderBy: { updatedAt: 'desc' },
include: {
project: { select: { name: true, color: true } },
somedayList: { select: { title: true } },
},
}),
prisma.weeklyGoal.findMany({
where: { userId },
select: { weekStart: true, text: true },
}),
]);
// Build week → goal map (weekStart is always Monday)
const goalByWeek = new Map<string, string>();
for (const g of goals) {
if (g.text && g.text !== DEFAULT_GOAL) {
goalByWeek.set(isoWeek(g.weekStart), g.text);
}
}
// Group tasks by ISO week
const byWeek = new Map<string, typeof tasks>();
for (const task of tasks) {
const key = isoWeek(task.updatedAt);
if (!byWeek.has(key)) byWeek.set(key, []);
byWeek.get(key)!.push(task);
}
return { byWeek, goalByWeek };
}
// ── CSV route ──────────────────────────────────────────────────────────────
export async function GET(request: Request) { export async function GET(request: Request) {
const session = await getServerSession(authOptions); const session = await getServerSession(authOptions);

View File

@ -10,7 +10,7 @@ import { ViewStyle, KanbanStage, Task } from "./WeeklyView";
import { AVAILABLE_FONTS, isCustomFont } from "../lib/fontConstants"; import { AVAILABLE_FONTS, isCustomFont } from "../lib/fontConstants";
import { translations } from "../lib/weeklyViewTranslations"; import { translations } from "../lib/weeklyViewTranslations";
import { WeatherDisplayKey, WEATHER_DISPLAY_DEFAULTS } from "../lib/weeklyViewConstants"; import { WeatherDisplayKey, WEATHER_DISPLAY_DEFAULTS } from "../lib/weeklyViewConstants";
import { EXPORT_FIELDS, type ExportFieldKey } from "../app/api/user/export/route"; import { EXPORT_FIELDS, type ExportFieldKey } from "@/lib/export-fields";
import { import {
ArrowLeftRight, ArrowLeftRight,
Briefcase, Briefcase,

View File

@ -105,9 +105,9 @@ import {
} from "@/lib/weekly/device-settings"; } from "@/lib/weekly/device-settings";
import type { DeviceViewSettingKey } from "@/lib/weekly/device-settings"; import type { DeviceViewSettingKey } from "@/lib/weekly/device-settings";
import { import {
stripHtml, DEFAULT_SOMEDAY_SLOT_COUNT, getSomedaySlotCount, stripHtml, DEFAULT_SOMEDAY_SLOT_COUNT, getSomedaySlotCount, invertColor,
isAllDayEvent, invertColor,
} from "@/lib/weekly/format"; } from "@/lib/weekly/format";
import { isAllDayEvent } from "@/lib/weekly/date-utils";
// Types // Types
import UserMenu from "./UserMenu"; import UserMenu from "./UserMenu";

170
src/lib/export-fields.ts Normal file
View File

@ -0,0 +1,170 @@
/**
* Felddefinitionen und Helfer für den Datenexport.
*
* Lagen bis v1.118.1 in `app/api/user/export/route.ts`. Next.js erlaubt in einer route.ts
* nur Route-Handler als Exporte — alles andere lässt die generierte Typprüfung auflaufen
* ("does not satisfy the constraint"). Ausserdem importierte SettingsSidebar EXPORT_FIELDS
* aus der Route und zog damit einen Server-Handler in den Client-Graphen.
*/
import { NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from "@/lib/auth";
import { prisma } from '@/lib/prisma';
// ── Field registry ─────────────────────────────────────────────────────────
export type ExportFieldKey =
| 'week'
| 'weekGoal'
| 'completedDate'
| 'scheduledDate'
| 'createdDate'
| 'project'
| 'list'
| 'priority'
| 'url'
| 'notes'
| 'description'
| 'delegatedTo'
| 'delegationNote'
| 'startTime'
| 'kanbanStage';
export const EXPORT_FIELDS: {
key: ExportFieldKey;
labelDe: string;
labelEn: string;
defaultOn: boolean;
csvOnly?: boolean; // not a table column, only used as a section in PDF/CSV
}[] = [
{ key: 'week', labelDe: 'Kalenderwoche', labelEn: 'Calendar week', defaultOn: true, csvOnly: true },
{ key: 'weekGoal', labelDe: 'Wochenziel', labelEn: 'Goal of the week', defaultOn: true, csvOnly: true },
{ key: 'completedDate', labelDe: 'Erledigt am', labelEn: 'Completed', defaultOn: true },
{ key: 'scheduledDate', labelDe: 'Geplant für', labelEn: 'Scheduled for', defaultOn: true },
{ key: 'createdDate', labelDe: 'Erstellt am', labelEn: 'Created', defaultOn: false },
{ key: 'project', labelDe: 'Projekt', labelEn: 'Project', defaultOn: true },
{ key: 'list', labelDe: 'Liste', labelEn: 'List', defaultOn: true },
{ key: 'priority', labelDe: 'Priorität', labelEn: 'Priority', defaultOn: true },
{ key: 'url', labelDe: 'Link / URL', labelEn: 'Link / URL', defaultOn: false },
{ key: 'notes', labelDe: 'Notiz', labelEn: 'Notes', defaultOn: false },
{ key: 'description', labelDe: 'Beschreibung', labelEn: 'Description', defaultOn: false },
{ key: 'delegatedTo', labelDe: 'Delegiert an', labelEn: 'Delegated to', defaultOn: false },
{ key: 'delegationNote', labelDe: 'Delegationsnotiz', labelEn: 'Delegation note', defaultOn: false },
{ key: 'startTime', labelDe: 'Uhrzeit', labelEn: 'Start time', defaultOn: false },
{ key: 'kanbanStage', labelDe: 'Kanban-Stage', labelEn: 'Kanban stage', defaultOn: false },
];
// ── Shared helpers (also used by export-pdf route) ─────────────────────────
export function isoWeek(date: Date): string {
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
const dayNum = d.getUTCDay() || 7;
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
const weekNo = Math.ceil((((d.getTime() - yearStart.getTime()) / 86400000) + 1) / 7);
return `${d.getUTCFullYear()}-W${String(weekNo).padStart(2, '0')}`;
}
export function weekMondaySunday(weekKey: string): { mon: Date; sun: Date } {
const [year, w] = weekKey.split('-W');
const d = new Date(Date.UTC(parseInt(year), 0, 1));
d.setUTCDate(d.getUTCDate() + (parseInt(w) - 1) * 7 - (d.getUTCDay() || 7) + 1);
const sun = new Date(d); sun.setUTCDate(d.getUTCDate() + 6);
return { mon: d, sun };
}
export function weekLabel(weekKey: string, de: boolean): string {
const [, w] = weekKey.split('-W');
const year = weekKey.split('-W')[0];
const { mon, sun } = weekMondaySunday(weekKey);
const fmt = (x: Date) => x.toLocaleDateString(de ? 'de-DE' : 'en-GB', { day: '2-digit', month: 'short', timeZone: 'UTC' });
return de
? `KW ${w} — ${year} (${fmt(mon)} – ${fmt(sun)})`
: `Week ${w} — ${year} (${fmt(mon)} – ${fmt(sun)})`;
}
export function fmtDate(d: Date | null | undefined, de: boolean): string {
if (!d) return '';
return de
? d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })
: d.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' });
}
export function priorityLabel(task: any, de: boolean): string {
if (task.urgency != null && task.importance != null) {
if (task.urgency && task.importance) return de ? 'Sofort erledigen' : 'Do now';
if (!task.urgency && task.importance) return de ? 'Planen' : 'Schedule';
if (task.urgency && !task.importance) return de ? 'Delegieren' : 'Delegate';
return de ? 'Eliminieren' : 'Eliminate';
}
if (task.priority) return task.priority;
return '';
}
export function priorityColor(task: any): string {
if (task.urgency != null && task.importance != null) {
if (task.urgency && task.importance) return '#ef4444';
if (!task.urgency && task.importance) return '#3b82f6';
if (task.urgency && !task.importance) return '#f97316';
return '#9ca3af';
}
const map: Record<string, string> = { A: '#ef4444', B: '#f97316', C: '#eab308', D: '#3b82f6', E: '#9ca3af' };
return task.priority ? (map[task.priority] || '#6b7280') : '#6b7280';
}
export function csvCell(value: string): string {
return `"${(value ?? '').replace(/"/g, '""').replace(/\n/g, ' ')}"`;
}
const DEFAULT_GOAL = 'your goal of this week';
// ── Fetch shared export data (also used by PDF route) ──────────────────────
export async function fetchExportData(userId: string, startDate: string | null, endDate: string | null) {
const where: any = { userId, completed: true, deletedAt: null };
if (startDate || endDate) {
where.updatedAt = {};
if (startDate) where.updatedAt.gte = new Date(startDate);
if (endDate) {
const end = new Date(endDate);
end.setHours(23, 59, 59, 999);
where.updatedAt.lte = end;
}
}
const [tasks, goals] = await Promise.all([
prisma.task.findMany({
where,
orderBy: { updatedAt: 'desc' },
include: {
project: { select: { name: true, color: true } },
somedayList: { select: { title: true } },
},
}),
prisma.weeklyGoal.findMany({
where: { userId },
select: { weekStart: true, text: true },
}),
]);
// Build week → goal map (weekStart is always Monday)
const goalByWeek = new Map<string, string>();
for (const g of goals) {
if (g.text && g.text !== DEFAULT_GOAL) {
goalByWeek.set(isoWeek(g.weekStart), g.text);
}
}
// Group tasks by ISO week
const byWeek = new Map<string, typeof tasks>();
for (const task of tasks) {
const key = isoWeek(task.updatedAt);
if (!byWeek.has(key)) byWeek.set(key, []);
byWeek.get(key)!.push(task);
}
return { byWeek, goalByWeek };
}
// ── CSV route ──────────────────────────────────────────────────────────────

View File

@ -20,19 +20,37 @@ export interface NotionEvent {
} }
/** /**
* Find all databases shared with the integration * ACHTUNG — Migration auf @notionhq/client v5, ungetestet.
*
* Der bisherige Code rief `notion.databases.query()` auf. Diese Methode gibt es in SDK v5
* nicht mehr (nur noch retrieve/create/update), der Aufruf lief also zur Laufzeit in ein
* "is not a function". Die Notion-Anbindung war damit defekt — aufgefallen erst, als der
* Typecheck scharf geschaltet wurde.
*
* Umgestellt auf `dataSources.query()`. Beides ist konsistent: Die Suche liefert jetzt
* data sources, deren `id` direkt als `data_source_id` taugt.
*
* WICHTIG: Bereits gespeicherte Notion-Verbindungen enthalten alte database_ids. Die sind
* KEINE data_source_ids — betroffene Nutzer müssen ihre Notion-Verbindung einmal neu
* einrichten. Mangels Notion-Zugang ist nichts davon praktisch erprobt.
*/
/**
* Find all data sources shared with the integration
*/ */
export async function getUserDatabases(accessToken: string): Promise<NotionDatabase[]> { export async function getUserDatabases(accessToken: string): Promise<NotionDatabase[]> {
const notion = new Client({ auth: accessToken }); const notion = new Client({ auth: accessToken });
// SDK v5: Was frueher "database" hiess, heisst jetzt "data source". Der alte Wert
// 'database' ist kein gueltiger Filterwert mehr.
const response = await notion.search({ const response = await notion.search({
filter: { property: 'object', value: 'database' }, filter: { property: 'object', value: 'data_source' },
page_size: 50, page_size: 50,
}); });
const databases: NotionDatabase[] = []; const databases: NotionDatabase[] = [];
for (const result of response.results) { for (const result of response.results) {
if (result.object !== 'database') continue; if (result.object !== 'data_source') continue;
const db = result as any; const db = result as any;
const title = db.title?.map((t: any) => t.plain_text).join('') || 'Untitled'; const title = db.title?.map((t: any) => t.plain_text).join('') || 'Untitled';
@ -79,8 +97,8 @@ export async function getUpcomingEvents(
let startCursor: string | undefined; let startCursor: string | undefined;
while (hasMore) { while (hasMore) {
const response = await notion.databases.query({ const response = await notion.dataSources.query({
database_id: databaseId, data_source_id: databaseId,
filter: { filter: {
and: [ and: [
{ {

View File

@ -55,7 +55,10 @@ export async function subscribeToPush(
try { try {
const subscription = await registration.pushManager.subscribe({ const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true, userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(vapidPublicKey), // Cast noetig: TypeScript typisiert Uint8Array seit 5.7 als Uint8Array<ArrayBufferLike>,
// BufferSource verlangt aber ArrayBuffer. urlBase64ToUint8Array liefert immer einen
// echten ArrayBuffer, der SharedArrayBuffer-Fall kann hier nicht auftreten.
applicationServerKey: urlBase64ToUint8Array(vapidPublicKey) as BufferSource,
}); });
return subscription; return subscription;
} catch (err) { } catch (err) {

27
src/types/web-push.d.ts vendored Normal file
View File

@ -0,0 +1,27 @@
/**
* Minimale Typdeklaration für `web-push`.
*
* Das Paket liefert keine eigenen Typen, und `@types/web-push` als Abhängigkeit aufzunehmen
* scheitert daran, dass `npm install` im Projektordner nicht läuft (CIFS-Freigabe ohne
* Symlink-Unterstützung). Deklariert ist nur, was das Projekt tatsächlich benutzt.
*/
declare module "web-push" {
export interface PushSubscription {
endpoint: string;
keys: { p256dh: string; auth: string };
}
export interface SendResult {
statusCode: number;
body: string;
headers: Record<string, string>;
}
export function setVapidDetails(subject: string, publicKey: string, privateKey: string): void;
export function sendNotification(
subscription: PushSubscription,
payload?: string | Buffer | null,
options?: Record<string, unknown>,
): Promise<SendResult>;
export function generateVAPIDKeys(): { publicKey: string; privateKey: string };
}

View File

@ -31,8 +31,8 @@
}, },
"include": [ "include": [
"next-env.d.ts", "next-env.d.ts",
"**/*.ts", "src/**/*.ts",
"**/*.tsx", "src/**/*.tsx",
".next/types/**/*.ts" ".next/types/**/*.ts"
], ],
"exclude": [ "exclude": [