Horoskop in kozmicni dogodki: dnevno AI generiranje + scheduler

- Nov dnevni scheduler (server/scheduler.ts): vsak dan ob zagonu in vsakih 6h
  preveri/generira horoskope in kozmicne dogodke (prej samo enkrat ob zagonu)
- Kozmicni dogodki so zdaj AI-generirani in dnevni (nova tabela cosmic_events
  + /api/cosmic-events), namesto hardcoded fiksnih datumov iz feb/mar 2026
- Naslovni horoskop widget bere pravi AI horoskop za danes (prej staticni tekst)
- Frontend: staleTime 30min + refetchOnWindowFocus za dnevno osvezevanje
This commit is contained in:
Folx Ops 2026-06-07 15:05:20 +00:00
parent 6c34d44cea
commit 365da96f5b
6 changed files with 204 additions and 8 deletions

View File

@ -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 (
<div className="flex gap-px">
@ -18,6 +25,12 @@ export function HoroscopeWidget() {
const [index, setIndex] = useState(0);
const [paused, setPaused] = useState(false);
const { data: aiHoroscopes = [] } = useQuery<AIHoroscope[]>({
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 (
<div
@ -88,7 +103,7 @@ export function HoroscopeWidget() {
</div>
<div className="px-3 pb-3 pt-2 flex-1 flex flex-col justify-between">
<p className="text-xs text-white/60 leading-relaxed line-clamp-5">{horoscope.general}</p>
<p className="text-xs text-white/60 leading-relaxed line-clamp-5">{generalText}</p>
<p className="text-[10px] text-amber-400 mt-2 group-hover:underline">Mehr lesen</p>
</div>

View File

@ -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<CosmicEvent[]>({
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 (
<section className="mb-10" data-testid="section-astro-events">
<h2 className="text-lg font-bold text-foreground flex items-center gap-2 mb-4">
@ -85,7 +103,7 @@ function AstroEventsSection() {
Aktuelle kosmische Ereignisse
</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{ASTRO_EVENTS.map((event, i) => {
{events.map((event, i) => {
const ec = getEventColor(event.type);
return (
<div
@ -378,6 +396,8 @@ export default function HoroscopePage() {
const { data: aiHoroscopes = [], isLoading: aiLoading } = useQuery<AIHoroscope[]>({
queryKey: ["/api/horoscopes/today"],
staleTime: 1000 * 60 * 30,
refetchOnWindowFocus: true,
});
useEffect(() => {

View File

@ -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<any | n
return null;
}
const VALID_ICONS = ["mercury", "venus", "moon", "newmoon", "sun", "saturn", "jupiter", "mars"];
const VALID_TYPES = ["retrograde", "moon", "transit", "season"];
export async function getCosmicEventsForToday(): Promise<any[]> {
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<void> {
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);
}
}

View File

@ -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<Server> {
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(/<channel>[\s\S]*?<title>(.*?)<\/title>/)?.[1]?.replace(/<!\[CDATA\[|\]\]>/g, "").trim() || "";
const items: { title: string; link: string; source: string; pubDate: string }[] = [];

28
server/scheduler.ts Normal file
View File

@ -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.");
}

View File

@ -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(),