perf: preload events and CalDAV config to avoid empty screens

Add CaldavConfigStore and preloadAppData() to load events (current month)
and CalDAV config into stores before dismissing the auth loading spinner.
This prevents the brief empty flash when first navigating to Calendar or
Settings tabs. Also applies Prettier formatting across codebase.
This commit is contained in:
2026-02-09 18:59:03 +01:00
parent 0e406e4dca
commit 868e1ba68d
22 changed files with 178 additions and 69 deletions

View File

@@ -83,7 +83,7 @@ app.use(
authController,
chatController,
eventController,
caldavController
caldavController,
}),
);

View File

@@ -25,7 +25,7 @@ export class MongoCaldavRepository implements CaldavRepository {
}
async deleteByUserId(userId: string): Promise<boolean> {
const result = await CaldavConfigModel.findOneAndDelete({userId});
const result = await CaldavConfigModel.findOneAndDelete({ userId });
return result !== null;
}
}

View File

@@ -28,7 +28,10 @@ export class MongoEventRepository implements EventRepository {
return events.map((e) => e.toJSON() as unknown as CalendarEvent);
}
async findByCaldavUUID(userId: string, caldavUUID: string): Promise<CalendarEvent | null> {
async findByCaldavUUID(
userId: string,
caldavUUID: string,
): Promise<CalendarEvent | null> {
const event = await EventModel.findOne({ userId, caldavUUID });
if (!event) return null;
return event.toJSON() as unknown as CalendarEvent;

View File

@@ -2,9 +2,7 @@ import mongoose, { Schema, Document, Model } from "mongoose";
import { CalendarEvent } from "@calchat/shared";
import { IdVirtual } from "./types";
export interface EventDocument
extends Omit<CalendarEvent, "id">,
Document {
export interface EventDocument extends Omit<CalendarEvent, "id">, Document {
toJSON(): CalendarEvent;
}

View File

@@ -6,7 +6,7 @@ import {
AuthController,
ChatController,
EventController,
CaldavController
CaldavController,
} from "../controllers";
import { createCaldavRoutes } from "./caldav.routes";

View File

@@ -363,9 +363,7 @@ async function getTestResponse(
event: {
title: sportEvent.title,
startTime: exceptionDate,
endTime: new Date(
exceptionDate.getTime() + 90 * 60 * 1000,
), // +90 min
endTime: new Date(exceptionDate.getTime() + 90 * 60 * 1000), // +90 min
description: sportEvent.description,
recurrenceRule: sportEvent.recurrenceRule,
exceptionDates: sportEvent.exceptionDates,
@@ -375,7 +373,8 @@ async function getTestResponse(
};
}
return {
content: "Ich konnte keinen Termin 'Sport' finden. Bitte erstelle ihn zuerst.",
content:
"Ich konnte keinen Termin 'Sport' finden. Bitte erstelle ihn zuerst.",
};
}
@@ -387,13 +386,13 @@ async function getTestResponse(
// Calculate UNTIL date: 6 weeks from start
const untilDate = new Date(sportEvent.startTime);
untilDate.setDate(untilDate.getDate() + 42); // 6 weeks
const untilStr = untilDate.toISOString().replace(/[-:]/g, "").split(".")[0] + "Z";
const untilStr =
untilDate.toISOString().replace(/[-:]/g, "").split(".")[0] + "Z";
const newRule = `FREQ=WEEKLY;BYDAY=WE;UNTIL=${untilStr}`;
return {
content:
"Alles klar! Ich beende die Sport-Serie nach 6 Wochen:",
content: "Alles klar! Ich beende die Sport-Serie nach 6 Wochen:",
proposedChanges: [
{
id: "sport-until",
@@ -413,7 +412,8 @@ async function getTestResponse(
};
}
return {
content: "Ich konnte keinen Termin 'Sport' finden. Bitte erstelle ihn zuerst.",
content:
"Ich konnte keinen Termin 'Sport' finden. Bitte erstelle ihn zuerst.",
};
}
@@ -440,9 +440,7 @@ async function getTestResponse(
event: {
title: sportEvent.title,
startTime: exceptionDate,
endTime: new Date(
exceptionDate.getTime() + 90 * 60 * 1000,
), // +90 min
endTime: new Date(exceptionDate.getTime() + 90 * 60 * 1000), // +90 min
description: sportEvent.description,
recurrenceRule: sportEvent.recurrenceRule,
exceptionDates: sportEvent.exceptionDates,
@@ -452,7 +450,8 @@ async function getTestResponse(
};
}
return {
content: "Ich konnte keinen Termin 'Sport' finden. Bitte erstelle ihn zuerst.",
content:
"Ich konnte keinen Termin 'Sport' finden. Bitte erstelle ihn zuerst.",
};
}
@@ -567,7 +566,11 @@ export class ChatService {
if (process.env.USE_TEST_RESPONSES === "true") {
// Test mode: use static responses
response = await getTestResponse(responseIndex, this.eventService, userId);
response = await getTestResponse(
responseIndex,
this.eventService,
userId,
);
responseIndex++;
} else {
// Production mode: use real AI
@@ -642,7 +645,11 @@ export class ChatService {
const createdEvent = await this.eventService.create(userId, event);
content = `Der Termin "${createdEvent.title}" wurde erstellt.`;
} else if (action === "update" && eventId && updates) {
const updatedEvent = await this.eventService.update(eventId, userId, updates);
const updatedEvent = await this.eventService.update(
eventId,
userId,
updates,
);
content = updatedEvent
? `Der Termin "${updatedEvent.title}" wurde aktualisiert.`
: "Termin nicht gefunden.";

View File

@@ -24,7 +24,10 @@ export class EventService {
return event;
}
async findByCaldavUUID(userId: string, caldavUUID: string): Promise<CalendarEvent | null> {
async findByCaldavUUID(
userId: string,
caldavUUID: string,
): Promise<CalendarEvent | null> {
return this.eventRepo.findByCaldavUUID(userId, caldavUUID);
}

View File

@@ -11,7 +11,10 @@ export interface AIContext {
currentDate: Date;
// Callback to load events from a specific date range
// Returns ExpandedEvent[] with occurrenceStart/occurrenceEnd for recurring events
fetchEventsInRange: (startDate: Date, endDate: Date) => Promise<ExpandedEvent[]>;
fetchEventsInRange: (
startDate: Date,
endDate: Date,
) => Promise<ExpandedEvent[]>;
// Callback to search events by title
searchEvents: (query: string) => Promise<CalendarEvent[]>;
// Callback to fetch a single event by ID

View File

@@ -8,7 +8,10 @@ export interface EventRepository {
startDate: Date,
endDate: Date,
): Promise<CalendarEvent[]>;
findByCaldavUUID(userId: string, caldavUUID: string): Promise<CalendarEvent | null>;
findByCaldavUUID(
userId: string,
caldavUUID: string,
): Promise<CalendarEvent | null>;
searchByTitle(userId: string, query: string): Promise<CalendarEvent[]>;
create(userId: string, data: CreateEventDTO): Promise<CalendarEvent>;
update(id: string, data: UpdateEventDTO): Promise<CalendarEvent | null>;