- 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
212 lines
8.3 KiB
TypeScript
212 lines
8.3 KiB
TypeScript
import OpenAI from "openai";
|
||
import { db } from "./db";
|
||
import { dailyHoroscopes, cosmicEvents } from "@shared/schema";
|
||
import { eq, and } from "drizzle-orm";
|
||
|
||
const openai = new OpenAI({
|
||
apiKey: process.env.AI_INTEGRATIONS_OPENAI_API_KEY,
|
||
baseURL: process.env.AI_INTEGRATIONS_OPENAI_BASE_URL,
|
||
});
|
||
|
||
const SIGN_NAMES = [
|
||
"Widder", "Stier", "Zwillinge", "Krebs", "Löwe", "Jungfrau",
|
||
"Waage", "Skorpion", "Schütze", "Steinbock", "Wassermann", "Fische"
|
||
];
|
||
|
||
function getTodayStr(): string {
|
||
const d = new Date();
|
||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
||
}
|
||
|
||
export async function getHoroscopesForToday(): Promise<any[]> {
|
||
const today = getTodayStr();
|
||
const existing = await db.select().from(dailyHoroscopes).where(eq(dailyHoroscopes.dateStr, today));
|
||
if (existing.length === 12) return existing;
|
||
return [];
|
||
}
|
||
|
||
export async function generateDailyHoroscopes(): Promise<void> {
|
||
const today = getTodayStr();
|
||
|
||
const existing = await db.select().from(dailyHoroscopes).where(eq(dailyHoroscopes.dateStr, today));
|
||
if (existing.length >= 12) {
|
||
console.log(`Horoscopes for ${today} already exist.`);
|
||
return;
|
||
}
|
||
|
||
console.log(`Generating horoscopes for ${today}...`);
|
||
|
||
for (let i = 0; i < SIGN_NAMES.length; i++) {
|
||
const signName = SIGN_NAMES[i];
|
||
|
||
const alreadyExists = existing.find(h => h.signIndex === i);
|
||
if (alreadyExists) continue;
|
||
|
||
try {
|
||
const response = await openai.chat.completions.create({
|
||
model: "gpt-5-mini",
|
||
messages: [
|
||
{
|
||
role: "system",
|
||
content: `Du bist ein erfahrener Astrologe, der tägliche Horoskope für eine deutschsprachige Volksmusik- und Schlager-Nachrichtenwebsite schreibt. Dein Stil ist warm, ermutigend und poetisch. Du beziehst manchmal Musik, Natur und alpine Kultur in deine Texte ein. Schreibe immer auf Deutsch. Das heutige Datum ist ${today}.`
|
||
},
|
||
{
|
||
role: "user",
|
||
content: `Erstelle ein ausführliches Tageshoroskop für das Sternzeichen ${signName} für heute (${today}).
|
||
|
||
Antworte NUR mit einem JSON-Objekt in diesem exakten Format (kein Markdown, keine Erklärung):
|
||
{
|
||
"general": "Ausführlicher allgemeiner Tagestext, mindestens 4-5 Sätze über die allgemeine Energie, Stimmung und Möglichkeiten des Tages.",
|
||
"love": "Ausführlicher Text über Liebe und Partnerschaft, mindestens 3-4 Sätze mit konkreten Ratschlägen für Singles und Paare.",
|
||
"career": "Ausführlicher Text über Beruf und Finanzen, mindestens 3-4 Sätze mit konkreten Tipps.",
|
||
"health": "Ausführlicher Text über Gesundheit und Wohlbefinden, mindestens 3-4 Sätze.",
|
||
"tip": "Ein konkreter, umsetzbarer Tipp des Tages in 1-2 Sätzen.",
|
||
"weekly": "Ausführliche Wochenvorschau, mindestens 4-5 Sätze mit Hinweisen für jeden Wochentag.",
|
||
"monthly": "Ausführliche Monatsvorschau, mindestens 4-5 Sätze über die wichtigsten Themen des Monats."
|
||
}`
|
||
}
|
||
],
|
||
max_completion_tokens: 2000,
|
||
});
|
||
|
||
const content = response.choices[0]?.message?.content || "";
|
||
const jsonMatch = content.match(/\{[\s\S]*\}/);
|
||
if (!jsonMatch) {
|
||
console.error(`Failed to parse horoscope for ${signName}`);
|
||
continue;
|
||
}
|
||
|
||
const parsed = JSON.parse(jsonMatch[0]);
|
||
|
||
await db.insert(dailyHoroscopes).values({
|
||
signIndex: i,
|
||
signName,
|
||
dateStr: today,
|
||
general: parsed.general || "",
|
||
love: parsed.love || "",
|
||
career: parsed.career || "",
|
||
health: parsed.health || "",
|
||
tip: parsed.tip || "",
|
||
weekly: parsed.weekly || "",
|
||
monthly: parsed.monthly || "",
|
||
});
|
||
|
||
console.log(`Generated horoscope for ${signName}`);
|
||
} catch (err: any) {
|
||
console.error(`Error generating horoscope for ${signName}:`, err.message);
|
||
}
|
||
}
|
||
|
||
console.log(`Horoscope generation complete for ${today}.`);
|
||
}
|
||
|
||
export async function getOrGenerateHoroscope(signIndex: number): Promise<any | null> {
|
||
const today = getTodayStr();
|
||
|
||
const [existing] = await db.select().from(dailyHoroscopes)
|
||
.where(and(eq(dailyHoroscopes.dateStr, today), eq(dailyHoroscopes.signIndex, signIndex)));
|
||
|
||
if (existing) return existing;
|
||
|
||
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);
|
||
}
|
||
}
|