diff --git a/client/src/components/horoscope-widget.tsx b/client/src/components/horoscope-widget.tsx index 48a7091..a5546f9 100644 --- a/client/src/components/horoscope-widget.tsx +++ b/client/src/components/horoscope-widget.tsx @@ -1,8 +1,15 @@ import { useState, useEffect, useCallback } from "react"; import { useLocation } from "wouter"; +import { useQuery } from "@tanstack/react-query"; import { Star, Heart, Briefcase, TrendingUp, ChevronLeft, ChevronRight } from "lucide-react"; import { SIGNS, ELEMENT_COLORS, getHoroscope, getRating } from "@/lib/horoscope-data"; +interface AIHoroscope { + signIndex: number; + signName: string; + general: string; +} + function MiniStars({ count, max = 5 }: { count: number; max?: number }) { return (
@@ -18,6 +25,12 @@ export function HoroscopeWidget() { const [index, setIndex] = useState(0); const [paused, setPaused] = useState(false); + const { data: aiHoroscopes = [] } = useQuery({ + queryKey: ["/api/horoscopes/today"], + staleTime: 1000 * 60 * 30, + refetchOnWindowFocus: true, + }); + const next = useCallback(() => setIndex((i) => (i + 1) % SIGNS.length), []); const prev = useCallback(() => setIndex((i) => (i - 1 + SIGNS.length) % SIGNS.length), []); @@ -28,7 +41,9 @@ export function HoroscopeWidget() { }, [paused, next]); const sign = SIGNS[index]; - const horoscope = getHoroscope(index); + // Prefer the real AI-generated horoscope for today; fall back to static text until loaded. + const aiForSign = aiHoroscopes.find((h) => h.signIndex === index); + const generalText = aiForSign?.general || getHoroscope(index).general; return (
-

{horoscope.general}

+

{generalText}

Mehr lesen

diff --git a/client/src/pages/horoscope.tsx b/client/src/pages/horoscope.tsx index 7053f57..58844cf 100644 --- a/client/src/pages/horoscope.tsx +++ b/client/src/pages/horoscope.tsx @@ -77,7 +77,25 @@ function getEventColor(type: string) { } } +interface CosmicEvent { + title: string; + description: string; + dateRange: string; + icon: string; + type: string; + affectedSigns: string[]; +} + function AstroEventsSection() { + const { data: apiEvents = [] } = useQuery({ + queryKey: ["/api/cosmic-events"], + staleTime: 1000 * 60 * 30, + refetchOnWindowFocus: true, + }); + + // Use live AI-generated events when available, otherwise fall back to static list. + const events = apiEvents.length > 0 ? apiEvents : ASTRO_EVENTS; + return (

@@ -85,7 +103,7 @@ function AstroEventsSection() { Aktuelle kosmische Ereignisse

- {ASTRO_EVENTS.map((event, i) => { + {events.map((event, i) => { const ec = getEventColor(event.type); return (
({ queryKey: ["/api/horoscopes/today"], + staleTime: 1000 * 60 * 30, + refetchOnWindowFocus: true, }); useEffect(() => { diff --git a/server/horoscope-generator.ts b/server/horoscope-generator.ts index 8eee5d1..6097dab 100644 --- a/server/horoscope-generator.ts +++ b/server/horoscope-generator.ts @@ -1,6 +1,6 @@ import OpenAI from "openai"; import { db } from "./db"; -import { dailyHoroscopes } from "@shared/schema"; +import { dailyHoroscopes, cosmicEvents } from "@shared/schema"; import { eq, and } from "drizzle-orm"; const openai = new OpenAI({ @@ -110,3 +110,102 @@ export async function getOrGenerateHoroscope(signIndex: number): Promise { + const today = getTodayStr(); + const rows = await db.select().from(cosmicEvents) + .where(eq(cosmicEvents.dateStr, today)); + return rows + .sort((a, b) => a.position - b.position) + .map((r) => ({ + title: r.title, + description: r.description, + dateRange: r.dateRange, + icon: r.icon, + type: r.type, + affectedSigns: JSON.parse(r.affectedSigns || "[]"), + })); +} + +export async function generateCosmicEvents(): Promise { + const today = getTodayStr(); + + const existing = await db.select().from(cosmicEvents) + .where(eq(cosmicEvents.dateStr, today)); + if (existing.length >= 6) { + console.log(`Cosmic events for ${today} already exist.`); + return; + } + + console.log(`Generating cosmic events for ${today}...`); + + const d = new Date(); + const monthName = d.toLocaleDateString("de-DE", { month: "long", year: "numeric" }); + + try { + const response = await openai.chat.completions.create({ + model: "gpt-5-mini", + messages: [ + { + role: "system", + content: `Du bist ein erfahrener Astrologe, der die aktuellen kosmischen Ereignisse (Planetenkonstellationen, Mondphasen, Retrograden, Tierkreis-Saison) für eine deutschsprachige Volksmusik- und Schlager-Website beschreibt. Du kennst die realen astronomischen Daten. Heute ist der ${today} (${monthName}). Beschreibe ausschließlich Ereignisse, die JETZT, rund um dieses Datum tatsächlich aktiv oder relevant sind. Verwende realistische, dem aktuellen Datum entsprechende Datumsangaben. Schreibe warm und zugänglich auf Deutsch.`, + }, + { + role: "user", + content: `Erstelle die 6 wichtigsten aktuellen kosmischen Ereignisse für heute (${today}). Dazu sollten gehören: die aktuelle Tierkreis-Saison (Sonne im Zeichen), die aktuelle Mondphase, eventuelle Retrograden, sowie relevante Planetentransite (Venus, Mars, Saturn, Jupiter). + +Antworte NUR mit einem JSON-Array von genau 6 Objekten (kein Markdown, keine Erklärung) in diesem exakten Format: +[ + { + "title": "Kurzer Titel, z.B. 'Vollmond im Skorpion' oder 'Zwillinge-Saison'", + "description": "3-4 Sätze über die Bedeutung dieses Ereignisses und seinen Einfluss.", + "dateRange": "Realistische Datumsangabe passend zum heutigen Datum, z.B. '21. Mai – 20. Juni 2026' oder '11. Juni 2026'", + "icon": "EINER VON: mercury, venus, moon, newmoon, sun, saturn, jupiter, mars", + "type": "EINER VON: retrograde, moon, transit, season", + "affectedSigns": ["3-4 betroffene Sternzeichen auf Deutsch, z.B. Widder, Stier, Zwillinge, Krebs, Löwe, Jungfrau, Waage, Skorpion, Schütze, Steinbock, Wassermann, Fische"] + } +]`, + }, + ], + max_completion_tokens: 2500, + }); + + const content = response.choices[0]?.message?.content || ""; + const jsonMatch = content.match(/\[[\s\S]*\]/); + if (!jsonMatch) { + console.error("Failed to parse cosmic events"); + return; + } + + const parsed = JSON.parse(jsonMatch[0]); + if (!Array.isArray(parsed) || parsed.length === 0) { + console.error("Cosmic events response is not a valid array"); + return; + } + + for (let i = 0; i < parsed.length; i++) { + const ev = parsed[i]; + const icon = VALID_ICONS.includes(ev.icon) ? ev.icon : "moon"; + const type = VALID_TYPES.includes(ev.type) ? ev.type : "transit"; + const signs = Array.isArray(ev.affectedSigns) ? ev.affectedSigns : []; + + await db.insert(cosmicEvents).values({ + dateStr: today, + position: i, + title: String(ev.title || "").slice(0, 120), + description: String(ev.description || ""), + dateRange: String(ev.dateRange || "").slice(0, 120), + icon, + type, + affectedSigns: JSON.stringify(signs), + }); + } + + console.log(`Generated ${parsed.length} cosmic events for ${today}.`); + } catch (err: any) { + console.error("Error generating cosmic events:", err.message); + } +} diff --git a/server/routes.ts b/server/routes.ts index b10b9b6..97dd706 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -3,7 +3,8 @@ import { createServer, type Server } from "http"; import { storage } from "./storage"; import { insertArticleSchema } from "@shared/schema"; import { seedDatabase } from "./seed"; -import { generateDailyHoroscopes, getHoroscopesForToday, getOrGenerateHoroscope } from "./horoscope-generator"; +import { generateDailyHoroscopes, getHoroscopesForToday, getOrGenerateHoroscope, getCosmicEventsForToday, generateCosmicEvents } from "./horoscope-generator"; +import { startDailyScheduler } from "./scheduler"; import { analyzeAllArticleImages, getCachedFocalPoints } from "./focal-point"; import { optimizeImage } from "./image-optimizer"; import { getAuthUrl, exchangeCodeForTokens, isConnected, fetchGalleryFromDropbox, getValidAccessToken, migrateGalleryToCloudinary } from "./dropbox"; @@ -58,9 +59,7 @@ export async function registerRoutes( ): Promise { await seedDatabase(); - generateDailyHoroscopes().catch((err) => - console.error("Background horoscope generation failed:", err.message) - ); + startDailyScheduler(); storage.getArticles().then((articles) => { analyzeAllArticleImages(articles).catch(err => @@ -682,6 +681,26 @@ export async function registerRoutes( } }); + // Cosmic events API + app.get("/api/cosmic-events", async (_req, res) => { + try { + const events = await getCosmicEventsForToday(); + res.json(events); + } catch (err: any) { + res.status(500).json({ message: err.message }); + } + }); + + app.post("/api/cosmic-events/generate", async (_req, res) => { + try { + await generateCosmicEvents(); + const events = await getCosmicEventsForToday(); + res.json({ generated: events.length, events }); + } catch (err: any) { + res.status(500).json({ message: err.message }); + } + }); + function parseRssItems(xml: string, maxAgeDays?: number): { title: string; link: string; source: string; pubDate: string }[] { const channelTitle = xml.match(/[\s\S]*?(.*?)<\/title>/)?.[1]?.replace(/<!\[CDATA\[|\]\]>/g, "").trim() || ""; const items: { title: string; link: string; source: string; pubDate: string }[] = []; diff --git a/server/scheduler.ts b/server/scheduler.ts new file mode 100644 index 0000000..d07780e --- /dev/null +++ b/server/scheduler.ts @@ -0,0 +1,28 @@ +import { generateDailyHoroscopes, generateCosmicEvents } from "./horoscope-generator"; + +async function runDailyGeneration() { + try { + await generateDailyHoroscopes(); + } catch (err: any) { + console.error("[scheduler] Horoscope generation failed:", err.message); + } + try { + await generateCosmicEvents(); + } catch (err: any) { + console.error("[scheduler] Cosmic events generation failed:", err.message); + } +} + +export function startDailyScheduler() { + // Run once at startup (generators are idempotent — they skip if today already exists). + runDailyGeneration(); + + // Re-check every 6 hours. Covers the day rollover without relying on container restarts. + const SIX_HOURS = 6 * 60 * 60 * 1000; + setInterval(() => { + console.log("[scheduler] Running scheduled daily generation check..."); + runDailyGeneration(); + }, SIX_HOURS); + + console.log("[scheduler] Daily horoscope + cosmic events scheduler started."); +} diff --git a/shared/schema.ts b/shared/schema.ts index 91d8e43..18ba3dd 100644 --- a/shared/schema.ts +++ b/shared/schema.ts @@ -50,6 +50,21 @@ export const dailyHoroscopes = pgTable("daily_horoscopes", { export type DailyHoroscope = typeof dailyHoroscopes.$inferSelect; +export const cosmicEvents = pgTable("cosmic_events", { + id: serial("id").primaryKey(), + dateStr: varchar("date_str", { length: 10 }).notNull(), + position: integer("position").notNull(), + title: varchar("title", { length: 120 }).notNull(), + description: text("description").notNull(), + dateRange: varchar("date_range", { length: 120 }).notNull(), + icon: varchar("icon", { length: 20 }).notNull(), + type: varchar("type", { length: 20 }).notNull(), + affectedSigns: text("affected_signs").notNull(), + createdAt: timestamp("created_at").notNull().defaultNow(), +}); + +export type CosmicEvent = typeof cosmicEvents.$inferSelect; + export const users = pgTable("users", { id: varchar("id").primaryKey().default(sql`gen_random_uuid()`), username: text("username").notNull().unique(),