+
+---
+
+## ποΈ Architecture Overview
+
+OpenStock leverages a resilient event-driven architecture powered by **Inngest**. We prioritize uptime for our generative features by utilizing a multi-provider AI strategy.
+
+### π§ Intelligent Model Routing
+
+We don't rely on a single point of failure. Our AI infrastructure automatically routes around outages.
+
+```mermaid
+graph LR
+ A[User Action / Cron] -->|Trigger| B(Inngest Function);
+ B --> C{Primary Provider};
+ C -->|Gemini 2.5 Flash Lite| D[Generate Content];
+ C -.->|Error / Rate Limit| E{Fallback Provider};
+ E -->|Siray.ai Ultra| D;
+ D --> F[Email / Notification];
+
+ style C fill:#20c997,stroke:#333,stroke-width:2px,color:black
+ style E fill:#3b82f6,stroke:#333,stroke-width:2px,color:white
+ style D fill:#fff,stroke:#333,stroke-width:2px,color:black
+```
+
+---
+
+## π€ AI Partners
+
+### Primary: Google Gemini
+The workhorse of our generative content. Fast, efficient, and deeply integrated via Inngest.
+
+### Fallback: Siray.ai
+> [!IMPORTANT]
+> **Zero Downtime Guarantee.**
+> When Gemini wavers, **Siray.ai** takes over instantly. No user request is ever dropped.
+
+
diff --git a/README.md b/README.md
index 00d354c..0552cca 100644
--- a/README.md
+++ b/README.md
@@ -409,9 +409,19 @@ OpenStock is and will remain free and open for everyone. This project is license
- [chinnsenn](https://github.com/chinnsenn) - Set up Docker configuration for the repository, ensuring a smooth development and deployment process.
- [koevoet1221](https://github.com/koevoet1221) - Resolved MongoDB Docker build issues, improving the projectβs overall stability and reliability.
+
+## β€οΈ Partners & Backers
+
+
+
+
+
+**[Siray.ai](https://www.siray.ai/)** β The robust AI infrastructure backing OpenStock. Siray.ai ensures our market insights never sleep.
+
## Special thanks
Huge thanks to [Adrian Hajdin (JavaScript Mastery)](https://github.com/adrianhajdin) β his excellent Stock Market App tutorial was instrumental in building OpenStock for the open-source community under the Open Dev Society.
GitHub: [adrianhajdin](https://github.com/adrianhajdin)
YouTube tutorial: [Stock Market App Tutorial](https://www.youtube.com/watch?v=gu4pafNCXng)
YouTube channel: [JavaScript Mastery](https://www.youtube.com/@javascriptmastery)
+
diff --git a/app/(root)/about/page.tsx b/app/(root)/about/page.tsx
new file mode 100644
index 0000000..e47294d
--- /dev/null
+++ b/app/(root)/about/page.tsx
@@ -0,0 +1,133 @@
+
+import React from 'react';
+import Image from 'next/image';
+import Link from 'next/link';
+import {
+ Users,
+ Globe,
+ Heart,
+ Code,
+ Github,
+ Twitter,
+ Linkedin,
+ ArrowRight
+} from 'lucide-react';
+
+export const metadata = {
+ title: 'About Us | OpenStock',
+ description: 'The story behind OpenStock and the Open Dev Society.',
+};
+
+export default function AboutPage() {
+ return (
+
+ {/* Hero Section */}
+
+
+
+
+
+
+
+
+ Tools for Everyone.
+
+
+ We believe financial intelligence shouldn't be locked behind paywalls.
+ OpenStock is built by the community, for the community.
+
+
+
+ {/* Mission Grid */}
+
+ }
+ title="Open Access"
+ desc="No premium tiers for core features. Real-time data and insights available to all, forever."
+ color="blue"
+ />
+ }
+ title="Open Source"
+ desc="Fully transparent codebase. Audit our algorithms, contribute features, and build with us."
+ color="purple"
+ />
+ }
+ title="Community Driven"
+ desc="Powered by donations and volunteers. We answer to our users, not shareholders."
+ color="red"
+ />
+
+
+ {/* Story Section */}
+
+
+
The Open Dev Society
+
+ OpenStock was born from a simple frustration: why are powerful financial tools so expensive?
+
+
+ We are a collective of developers, designers, and financial enthusiasts working under the Open Dev Society banner. Our mission is to democratize software by building high-quality, open-source alternatives to proprietary platforms.
+
+ );
+}
+
+function SocialButton({ href, icon, label }: any) {
+ return (
+
+ {icon}
+ {label}
+
+ );
+}
diff --git a/app/(root)/api-docs/page.tsx b/app/(root)/api-docs/page.tsx
index b23eab8..b24f07e 100644
--- a/app/(root)/api-docs/page.tsx
+++ b/app/(root)/api-docs/page.tsx
@@ -1,76 +1,247 @@
-import { Metadata } from 'next';
-export const metadata: Metadata = {
- title: 'API Documentation - OpenStock',
- description: 'Free and open API documentation for OpenStock platform - no paywalls, no barriers',
+import React from 'react';
+import Image from 'next/image';
+import Link from 'next/link';
+import {
+ Server,
+ Cpu,
+ ShieldCheck,
+ Clock,
+ Database,
+ Mail,
+ BarChart2,
+ Zap,
+ ArrowRight,
+ CheckCircle2,
+ AlertTriangle
+} from 'lucide-react';
+
+export const metadata = {
+ title: 'API & Architecture | OpenStock',
+ description: 'Technical documentation for OpenStock architecture, AI integrations, and background jobs.',
};
export default function ApiDocsPage() {
return (
-
-
-
Free & Open API Documentation
-
- Complete guide to integrating with the OpenStock API - completely free, forever
-
-
-
- π‘ Open Dev Society Promise: This API will always be free. No hidden costs, no usage limits for personal projects, no barriers to knowledge.
-
+
+ {/* Hero Section */}
+
+
+
+
+
+ +
+
+
+
-
-
- {/* Philosophy */}
-
-
π Our API Philosophy
-
- We believe market data should be accessible to everyone - students building their first portfolio tracker,
- developers creating tools for their community, and anyone who wants to learn about finance without barriers.
+
+ OpenStock Architecture
+
+
+ A transparent look at the event-driven, multi-provider system powering your market insights.
+
+
+
+ v1.0.0 Active
+ Gemini + Siray AI
+ Open Source AGPL-3.0
+
+
+
+ {/* AI Architecture Section */}
+
+
+
+
+
Intelligent UI
+
+
+ We prioritize uptime for generative features (Welcome Emails, News Summaries) using a robust
+ multi-provider strategy. Our system automatically routes around outages.
-
-
β Always Free: Core features remain free forever
-
β No Gatekeeping: Simple authentication, clear documentation
-
β Community First: Built for learners, students, and builders
-
β Open Source: API examples and SDKs are open source
-
-
- {/* Community Support */}
-
-
π€ Community & Support
-
-
-
π For Students
-
- Building a project for class? Email us at opendevsociety@cc.cc for unlimited access and mentorship.
-
+
+
+
+
+
+
+
+ Primary: Google Gemini
+ Flash Lite 2.5
+
+
+ Handles high-volume inference for news summarization and personalization.
+
+
-
-
π» For Developers
-
- Join our Discord community for code examples, troubleshooting, and collaboration opportunities.
-
+
+
+
+
+
+
+
+
+
+ Fallback: Siray.ai
+ Ultra 1.0
+
+
+ Instant failover protection. If Gemini wavers, Siray takes over to ensure zero dropped requests.
+
+
-
+
- {/* Open Source Commitment */}
-
-
π Open Source Promise
-
- This API, its documentation, and all example code are open source.
- Found a bug? Want a feature? Submit a PR or issue on GitHub.
-
+
+ );
+}
diff --git a/app/(root)/help/page.tsx b/app/(root)/help/page.tsx
index b7217ca..92f5bd1 100644
--- a/app/(root)/help/page.tsx
+++ b/app/(root)/help/page.tsx
@@ -1,123 +1,124 @@
import { Metadata } from 'next';
-// Removed unused lucide-react imports
+import {
+ HelpCircle,
+ MessageCircle,
+ BookOpen,
+ Lightbulb,
+ Mail,
+ Github,
+ ChevronDown
+} from 'lucide-react';
export const metadata: Metadata = {
- title: 'Help Center - OpenStock',
- description: 'Free help and community support - no barriers, just guidance',
+ title: 'Help Center | OpenStock',
+ description: 'Community-driven support for OpenStock. No paywalls, just help.',
};
export default function HelpPage() {
const faqs = [
{
question: "Is OpenStock really free forever?",
- answer: "Yes! We're part of the Open Dev Society, which means we'll never lock knowledge behind paywalls. Core features remain free always. We run on community donations and the belief that financial tools should be accessible to everyone."
+ answer: "Yes! We run on donations and community contribution. Core features (tracking, alerts, analysis) will remain free. We believe financial tools shouldn't be luxury items."
},
{
- question: "I'm a student - can I use this for my projects?",
- answer: "Absolutely! That's exactly why we built this. Use it for school projects, learning, or building your portfolio. Need help? Our community loves mentoring students. Email student@opendevsociety.org for extra support."
+ question: "How do I add stocks to my watchlist?",
+ answer: "Use the search bar at the top or in the header to find a company. On the stock's detail page, click the 'Heart' or 'Star' icon to instantly add it to your dashboard."
},
{
- question: "How do I add stocks to my favorites?",
- answer: "Navigate to any stock page and click the star icon. You can also search using the search bar and add directly from results. Everything is designed to be intuitive - no complex tutorials needed."
+ question: "Where does the market data come from?",
+ answer: "We partner with Finnhub and other providers to offer real-time and delayed data. While robust, please use it for analysis rather than high-frequency trading."
},
{
- question: "Can I contribute to OpenStock?",
- answer: "We'd love that! OpenStock is open source and community-driven. Check our GitHub for issues marked 'good first issue' or 'help wanted'. Every contribution, no matter how small, makes a difference."
+ question: "Can I contribute code or designs?",
+ answer: "Absolutely! Check our GitHub repository. We label issues as 'good first issue' for beginners. We welcome designers, developers, and writers alike."
},
{
- question: "What if I find a bug or have a feature request?",
- answer: "Please tell us! Submit issues on GitHub, join our Discord, or email opendevsociety@gmail.com. We see every report as a chance to make the platform better for everyone."
+ question: "My alerts aren't triggering.",
+ answer: "Alerts run every 5 minutes via our background jobs. Ensure you've confirmed your email address, as we send notifications primarily via email."
}
];
return (
-
-
-
Community Help Center
-
- Free help, guided by community, powered by the belief that everyone deserves support
-
-
-
- π€ Our Promise: Every question matters. Every beginner is welcomed. No exclusion, ever.
-
+
+
+ {/* Header */}
+
+
+
+
How can we help?
+
Community-powered support for everyone.
-
- {/* Help Philosophy */}
-
-
-
-
Learn Together
-
- Every expert was once a beginner. Our guides are written by the community, for the community.
- No jargon, no assumptions about prior knowledge.
-
-
-
-
-
-
Community Support
-
- Real people helping real people. Our Discord community includes students, professionals,
- and mentors who genuinely want to help you succeed.
-
-
-
-
-
-
Built with Care
-
- Every feature is designed with accessibility and ease-of-use in mind.
- We believe powerful tools should be simple to use.
-
-
+ {/* Quick Action Grid */}
+
+ }
+ title="Read Docs"
+ desc="Deep dive into features and API integration."
+ link="/api-docs"
+ linkText="View Documentation"
+ />
+ }
+ title="Community Chat"
+ desc="Get real-time answers from other users."
+ link="https://discord.gg/JkJ8kfxgxB"
+ linkText="Join Discord"
+ />
+ }
+ title="Report Bugs"
+ desc="Found an issue? Let our developers know."
+ link="https://github.com/Open-Dev-Society/OpenStock/issues"
+ linkText="Open Issue"
+ />
- {/* Community FAQs */}
-
-
Community Questions
-
- {faqs.map((faq, index) => (
-
-
{faq.question}
-
{faq.answer}
+ {/* FAQs */}
+
+
Frequently Asked Questions
+
+ {faqs.map((faq, idx) => (
+
+
+
+ {faq.question}
+
+
+ {faq.answer}
+
))}
-
+
- {/* Community Connection */}
-
-
Join Our Community
-
- Don't struggle alone. Our community of builders, learners, and dreamers is here to help.
- Because we believe the future belongs to those who build it openly.
-
@@ -42,7 +52,12 @@ export default async function StockDetails({ params }: StockDetailsPageProps) {
{/* Right column */}
-
+
-
-
Terms of Service
-
-
- Last updated: October 4, 2025
-
-
-
-
- π€ Written in Plain English: No legal jargon here. These terms are designed to be fair,
- understandable, and aligned with our Open Dev Society values.
-
+
+
+ {/* Hero */}
+
+
+
+
Terms of Service
+
+ Built on trust, transparency, and community values. No hidden gotchas, just clear rules.
+
+
Last updated: October 2025
-
- {/* Our Approach */}
-
-
π Our Approach to Terms
-
- We believe terms of service should protect both users and creators without being exploitative.
- These terms reflect the Open Dev Society manifesto: open, fair, community-first.
-
-
-
β No Gotchas: What you see is what you get
-
β Community Input: These terms were reviewed by our community
-
β Fair Use: Reasonable limits that protect everyone
-
β Always Free Core: We promise core features stay free forever
-
-
-
-
-
π― The Basics
-
- By using OpenStock, you're joining our community. Here's what that means:
-
-
-
-
π Respectful Use: Use OpenStock to learn, build, and grow - not to harm others
-
π Educational Focus: Perfect for students, personal projects, and learning
-
π€ Community Spirit: Help others when you can, ask for help when you need it
-
π Open Source Values: Contribute back when possible, share knowledge freely
-
+
+ {/* Core Philosophy */}
+
+
+
+ Our Promise
+
+
+
+
+
+
-
-
π° Our Free Forever Promise
-
-
Core features of OpenStock will always be free:
-
-
β Real-time stock data and charts
-
β Personal watchlists and portfolio tracking
-
β Basic market analysis tools
-
β Community features and discussions
-
β API access for personal projects
-
-
- This isn't a "freemium trap" - it's our commitment to making financial tools accessible to everyone.
-
-
-
-
-
-
π‘οΈ Investment Disclaimer (The Important Stuff)
-
-
Let's be crystal clear about this:
-
-
- OpenStock is an educational and analysis tool, not investment advice.
- We provide data and tools to help you make informed decisions, but the decisions are yours.
-
-
- We're not financial advisors. We're developers and community members who built
- tools we wished existed when we were learning about investing.
-
-
- Always do your own research. Use multiple sources, consult professionals,
- and never invest more than you can afford to lose.
+ {/* Disclaimer */}
+
+
+
+
+
Investment Disclaimer
+
+ **OpenStock is an educational and analysis tool, not a financial advisor.**
+ Data is provided "as is" for informational purposes. Never invest money you cannot afford to lose.
+ Always conduct your own research or consult a certified professional before making financial decisions.
-
-
π₯ Your Account & Responsibilities
-
- We trust you to be a good community member. Here's what we ask:
-
-
-
-
β¨ What We'd Love
-
-
β’ Share knowledge with other users
-
β’ Report bugs and suggest improvements
-
β’ Keep your account information current
-
β’ Use the platform to learn and grow
+ {/* User Responsibilities */}
+
+
Community Rules
+
+
+
β Do's
+
+
Share knowledge freely
+
Use API for personal projects
+
Respect other members
-
-
β What Hurts Everyone
-
-
β’ Sharing accounts or API keys
-
β’ Trying to break or exploit the system
-
β’ Harassing other community members
-
β’ Using the platform for illegal activities
+
+
β Don'ts
+
+
Γ Scrape data excessively
+
Γ Share API keys
+
Γ Use for high-frequency trading
-
-
π Data & Content
-
-
- Your data belongs to you. We provide tools to export everything anytime.
- We'll never claim ownership of your watchlists, notes, or personal information.
-
-
- Market data comes from licensed sources. While we provide it for free,
- please respect that it's meant for personal use and learning.
-
-
- Community contributions are appreciated. If you share insights or contribute
- to discussions, you're helping build a knowledge commons for everyone.
-
-
-
-
-
-
π§ Service Availability
-
- We're committed to keeping OpenStock running, but we're also realistic:
-
-
-
β’ We aim for 99.9% uptime, but stuff happens (we're human!)
-
β’ We'll give advance notice for planned maintenance
-
β’ Major outages will be communicated on our status page and Discord
-
β’ We're building sustainable infrastructure, not just cheap hosting
-
-
-
-
-
π Changes to These Terms
-
-
- We believe in transparency for terms changes too:
-
-
-
β’ Community discussion on proposed changes
-
β’ Clear explanation of what's changing and why
-
β’ Version history available on GitHub
-
-
-
-
-
-
π€ Questions or Concerns?
-
- Legal documents shouldn't be mysterious. If anything here confuses you or seems unfair,
- let's talk about it.
-
+ );
+}
diff --git a/app/(root)/watchlist/page.tsx b/app/(root)/watchlist/page.tsx
new file mode 100644
index 0000000..08ad167
--- /dev/null
+++ b/app/(root)/watchlist/page.tsx
@@ -0,0 +1,99 @@
+import React, { Suspense } from 'react';
+import { auth } from '@/lib/better-auth/auth';
+import { headers } from 'next/headers';
+import { redirect } from 'next/navigation';
+import { getUserWatchlist, isStockInWatchlist, removeFromWatchlist } from '@/lib/actions/watchlist.actions';
+import { getUserAlerts } from '@/lib/actions/alert.actions';
+import { getNews } from '@/lib/actions/finnhub.actions';
+import TradingViewWatchlist from '@/components/watchlist/TradingViewWatchlist';
+import WatchlistStockChip from '@/components/watchlist/WatchlistStockChip';
+import AlertsPanel from '@/components/watchlist/AlertsPanel';
+import NewsGrid from '@/components/watchlist/NewsGrid';
+import SearchCommand from '@/components/SearchCommand';
+import { Loader2 } from 'lucide-react';
+
+export default async function WatchlistPage() {
+ const session = await auth.api.getSession({
+ headers: await headers()
+ });
+
+ if (!session) {
+ redirect('/sign-in');
+ }
+
+ const userId = session.user.id;
+
+ // Parallel data fetching
+ // Parallel data fetching
+ const [watchlistItems, alerts, news] = await Promise.all([
+ getUserWatchlist(userId),
+ getUserAlerts(userId),
+ getNews() // Initial news fetch, maybe refine later to use watchlist symbols
+ ]);
+
+ const watchlistSymbols = watchlistItems.map((item: any) => item.symbol);
+ // const watchlistData = await getWatchlistData(watchlistSymbols); // OPTIMIZATION: Removed to prevent 429 errors. Widget handles data.
+
+ // Fallback news if watchlist has items
+ const relevantNews = watchlistSymbols.length > 0 ? await getNews(watchlistSymbols) : news;
+
+ return (
+
+ {/* Header */}
+
+
+
+ Watchlist
+
+
Track your favorite stocks and manage alerts.
+
+
+
+
+
+
+
+ {/* Main Content - Watchlist Table */}
+
+
+ {/* Manage Watchlist Section */}
+
+
+ Manage Symbols
+ {watchlistSymbols.length}
+
+ {watchlistSymbols.length > 0 ? (
+
+ {watchlistItems.map((item: any) => (
+
+ ))}
+
+ ) : (
+
No stocks in watchlist.
+ )}
+
+
+ {/* TradingView Widget */}
+
+
+
+
+
+ {/* News Section */}
+
}>
+
+
+
+
+ {/* Sidebar - Alerts */}
+
+
+
+
+
+ );
+}
diff --git a/app/api/inngest/route.ts b/app/api/inngest/route.ts
index 55532b9..caf822a 100644
--- a/app/api/inngest/route.ts
+++ b/app/api/inngest/route.ts
@@ -1,8 +1,8 @@
-import {serve} from "inngest/next";
-import {inngest} from "@/lib/inngest/client";
-import {sendDailyNewsSummary, sendSignUpEmail} from "@/lib/inngest/functions";
+import { serve } from "inngest/next";
+import { inngest } from "@/lib/inngest/client";
+import { sendWeeklyNewsSummary, sendSignUpEmail, checkStockAlerts, checkInactiveUsers } from "@/lib/inngest/functions";
-export const {GET, POST, PUT } = serve({
+export const { GET, POST, PUT } = serve({
client: inngest,
- functions: [sendSignUpEmail, sendDailyNewsSummary],
+ functions: [sendSignUpEmail, sendWeeklyNewsSummary, checkStockAlerts, checkInactiveUsers],
})
\ No newline at end of file
diff --git a/components/Footer.tsx b/components/Footer.tsx
index 92088eb..6ff6ab1 100644
--- a/components/Footer.tsx
+++ b/components/Footer.tsx
@@ -18,9 +18,15 @@ const Footer = () => {
className="brightness-0 invert"
/>
-
+
OpenStock is an open-source alternative to expensive market platforms. Track real-time prices, set personalized alerts, and explore detailed company insights β built openly, for everyone, forever free.
+
+
+ Learn about our mission
+ β
+
+
{
+
+
+
+
+ {/* Using the copied logo */}
+
+
+
+ β’ Reliably backed by Siray.ai
+
+
+
+ Ensuring 100% AI uptime for your market insights
+
+
+
+
+
+
+ );
+}
diff --git a/components/WatchlistButton.tsx b/components/WatchlistButton.tsx
index 1e53e55..f94f48d 100644
--- a/components/WatchlistButton.tsx
+++ b/components/WatchlistButton.tsx
@@ -1,43 +1,87 @@
"use client";
import React, { useMemo, useState } from "react";
+import { addToWatchlist, removeFromWatchlist } from "@/lib/actions/watchlist.actions";
+import { toast } from "sonner";
+interface WatchlistButtonProps {
+ symbol: string;
+ company: string;
+ isInWatchlist: boolean;
+ showTrashIcon?: boolean;
+ type?: "button" | "icon";
+ userId?: string; // Made optional for backward compat, but required for actions
+ onWatchlistChange?: (symbol: string, added: boolean) => void;
+}
const WatchlistButton = ({
- symbol,
- company,
- isInWatchlist,
- showTrashIcon = false,
- type = "button",
- onWatchlistChange,
- }: WatchlistButtonProps) => {
+ symbol,
+ company,
+ isInWatchlist,
+ showTrashIcon = false,
+ type = "button",
+ userId,
+ onWatchlistChange,
+}: WatchlistButtonProps) => {
const [added, setAdded] = useState(!!isInWatchlist);
+ const [loading, setLoading] = useState(false);
const label = useMemo(() => {
if (type === "icon") return added ? "" : "";
return added ? "Remove from Watchlist" : "Add to Watchlist";
}, [added, type]);
- const handleClick = () => {
+ const handleClick = async (e: React.MouseEvent) => {
+ e.preventDefault(); // Prevent link navigation if inside a link
+
+ if (!userId && !onWatchlistChange) {
+ console.error("WatchlistButton: userId or onWatchlistChange is required");
+ toast.error("Please sign in to modify watchlist");
+ return;
+ }
+
const next = !added;
- setAdded(next);
- onWatchlistChange?.(symbol, next);
+ setAdded(next); // Optimistic update
+ setLoading(true);
+
+ try {
+ if (userId) {
+ if (next) {
+ await addToWatchlist(userId, symbol, company);
+ toast.success(`${symbol} added to watchlist`);
+ } else {
+ await removeFromWatchlist(userId, symbol);
+ toast.success(`${symbol} removed from watchlist`);
+ }
+ }
+
+ // Call external handler if provided (e.g. for UI refresh)
+ onWatchlistChange?.(symbol, next);
+ } catch (error) {
+ console.error("Watchlist action failed:", error);
+ setAdded(!next); // Revert on error
+ toast.error("Failed to update watchlist");
+ } finally {
+ setLoading(false);
+ }
};
if (type === "icon") {
return (
);
};
diff --git a/components/watchlist/AlertsPanel.tsx b/components/watchlist/AlertsPanel.tsx
new file mode 100644
index 0000000..9d82743
--- /dev/null
+++ b/components/watchlist/AlertsPanel.tsx
@@ -0,0 +1,72 @@
+"use client";
+
+import React from "react";
+import { Trash2, TrendingUp, Bell } from "lucide-react";
+import { formatCurrency } from "@/lib/utils";
+import { deleteAlert } from "@/lib/actions/alert.actions";
+
+interface AlertsPanelProps {
+ alerts: any[];
+ onRefresh?: () => void;
+}
+
+export default function AlertsPanel({ alerts, onRefresh }: AlertsPanelProps) {
+ const handleDelete = async (id: string) => {
+ if (confirm("Are you sure you want to delete this alert?")) {
+ await deleteAlert(id);
+ if (onRefresh) onRefresh();
+ }
+ };
+
+ return (
+
+
+
+ `;
+
+ console.log(`π’ Sending Weekly News Broadcast to all subscribers`);
+ const broadcastResult = await kit.sendBroadcast(subject, content);
+ console.log("π Kit API Response:", JSON.stringify(broadcastResult, null, 2));
+ return { success: true, kitResponse: broadcastResult };
+ })
+
+ return { success: true, message: 'Weekly news broadcast sent' }
+ }
+)
+
+export const checkStockAlerts = inngest.createFunction(
+ { id: 'check-stock-alerts' },
+ { cron: '*/5 * * * *' }, // Run every 5 minutes
+ async ({ step }) => {
+ // Step 1: Fetch active alerts
+ const activeAlerts = await step.run('fetch-active-alerts', async () => {
+ // Dynamic import to avoid circular dep issues if any, or just standard import
+ const { connectToDatabase } = await import("@/database/mongoose");
+ const { Alert } = await import("@/database/models/alert.model");
+
+ await connectToDatabase();
+ const now = new Date();
+
+ return await Alert.find({
+ active: true,
+ triggered: false,
+ expiresAt: { $gt: now }
+ }).lean();
+ });
+
+ if (!activeAlerts || activeAlerts.length === 0) {
+ return { message: 'No active alerts to check.' };
+ }
+
+ // Step 2: Group by symbol
+ const symbols = [...new Set(activeAlerts.map((a: any) => a.symbol))];
+
+ // Step 3: Fetch prices
+ const prices = await step.run('fetch-prices', async () => {
+ const { getQuote } = await import("@/lib/actions/finnhub.actions");
+ const priceMap: Record = {};
+
+ // Process in chunks to be safe
+ for (const sym of symbols) {
+ try {
+ const quote = await getQuote(sym as string);
+ if (quote && quote.c) {
+ priceMap[sym as string] = quote.c;
+ }
+ } catch (e) {
+ console.error(`Failed to fetch price for ${sym}`, e);
+ }
+ }
+ return priceMap;
+ });
+
+ // Step 4: Check conditions
+ type TriggeredAlert = { alert: any; currentPrice: number };
+ const triggeredAlerts: TriggeredAlert[] = [];
+
+ for (const alert of activeAlerts as any[]) {
+ const currentPrice = prices[alert.symbol];
+ if (!currentPrice) continue;
+
+ let isTriggered = false;
+ // Simple check
+ if (alert.condition === 'ABOVE' && currentPrice >= alert.targetPrice) {
+ isTriggered = true;
+ } else if (alert.condition === 'BELOW' && currentPrice <= alert.targetPrice) {
+ isTriggered = true;
+ }
+
+ if (isTriggered) {
+ triggeredAlerts.push({ alert, currentPrice });
}
}
- // Step #4: (placeholder) Send the emails
- await step.run('send-news-emails', async () => {
- const results = await Promise.allSettled(
- userNewsSummaries.map(async ({ user, newsContent}) => {
- if(!newsContent) {
- console.log(`βοΈ Skipping email for ${user.email} - no news content`);
- return false;
- }
+ // Step 5: Process triggers
+ if (triggeredAlerts.length > 0) {
+ await step.run('process-triggered-alerts', async () => {
+ const { connectToDatabase } = await import("@/database/mongoose");
+ const { Alert } = await import("@/database/models/alert.model");
+ // In a real app we would import 'kit' here and use kit.sendBroadcast or similar
+ // For now, we just log it as the critical logic is the detection
+ await connectToDatabase();
- try {
- console.log(`π§ Attempting to send news summary email to: ${user.email}`);
- const result = await sendNewsSummaryEmail({ email: user.email, date: getFormattedTodayDate(), newsContent });
- console.log(`β News summary email sent successfully to: ${user.email}`);
- return result;
- } catch (error) {
- console.error(`β Failed to send news summary email to ${user.email}:`, error);
- throw error;
- }
- })
- );
-
- const successful = results.filter(r => r.status === 'fulfilled').length;
- const failed = results.filter(r => r.status === 'rejected').length;
- console.log(`π Email sending summary: ${successful} successful, ${failed} failed`);
- })
+ for (const { alert, currentPrice } of triggeredAlerts) {
+ console.log(`π ALERT FIRED: ${alert.symbol} is ${currentPrice} (${alert.condition} ${alert.targetPrice})`);
- return { success: true, message: 'Daily news summary emails sent successfully' }
+ // Mark triggered
+ await Alert.findByIdAndUpdate(alert._id, { triggered: true, active: false });
+ }
+ });
+ }
+
+ return {
+ processed: activeAlerts.length,
+ triggered: triggeredAlerts.length
+ };
}
-)
\ No newline at end of file
+);
+
+export const checkInactiveUsers = inngest.createFunction(
+ { id: 'check-inactive-users' },
+ { cron: '0 10 * * *' }, // Run every day at 10 AM
+ async ({ step }) => {
+ // Step 1: Fetch Inactive Users
+ const inactiveUsers = await step.run('fetch-inactive-users', async () => {
+ const { connectToDatabase } = await import("@/database/mongoose");
+ const mongoose = await connectToDatabase();
+ const db = mongoose.connection.db;
+ if (!db) throw new Error("No DB Connection");
+
+ const thirtyDaysAgo = new Date();
+ thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
+
+ // Criteria:
+ // 1. lastActiveAt < 30 days ago OR (undefined and createdAt < 30 days ago)
+ // 2. lastReengagementSentAt < 30 days ago OR undefined (don't spam)
+ const users = await db.collection('user').find({
+ $and: [
+ {
+ $or: [
+ { lastActiveAt: { $lt: thirtyDaysAgo } },
+ { lastActiveAt: { $exists: false }, createdAt: { $lt: thirtyDaysAgo } }
+ ]
+ },
+ {
+ $or: [
+ { lastReengagementSentAt: { $exists: false } },
+ { lastReengagementSentAt: { $lt: thirtyDaysAgo } }
+ ]
+ }
+ ]
+ }, { projection: { email: 1, name: 1, _id: 1 } }).limit(50).toArray(); // Limit 50 per run for safety
+
+ return users.map(u => ({ email: u.email, name: u.name, id: u._id.toString() }));
+ });
+
+ if (inactiveUsers.length === 0) {
+ return { message: "No inactive users found." };
+ }
+
+ // Step 2: Send Emails
+ const results = await step.run('send-reengagement-emails', async () => {
+ const { kit } = await import("@/lib/kit");
+ const { connectToDatabase } = await import("@/database/mongoose");
+ const mongoose = await connectToDatabase();
+ const db = mongoose.connection.db;
+
+ const sent: string[] = [];
+
+ for (const user of inactiveUsers) {
+ if (!user.email) continue;
+
+ const firstName = user.name ? user.name.split(' ')[0] : 'Indiestocker';
+ const subject = `π ${firstName}, opportunities are waiting for you`;
+
+ // --- HTML TEMPLATE (Teal) ---
+ const content = `
+
+
+
+
+
+
+
+
+
+
+
+ π OpenStock
+
+
+
+
We Miss You, ${firstName}
+
+
+ Hi ${firstName},
+ We noticed you haven't visited OpenStock in a while. The markets have been moving, and there might be some opportunities you don't want to miss!
+
+
+
+
+
Market Update
+
+ Markets have been active lately! Major indices have seen significant movements, and there might be opportunities in your tracked stocks that you don't want to miss.
+
+
+
+
+ Your watchlists are still active and ready to help you stay on top of your investments. Don't let market opportunities pass you by!
+