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:
parent
6c34d44cea
commit
365da96f5b
@ -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>
|
||||
|
||||
|
||||
@ -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(() => {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
28
server/scheduler.ts
Normal 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.");
|
||||
}
|
||||
@ -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(),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user