“For me, OpenStock isn’t just another stock app. It’s about giving people clarity and control in the market, without barriers or subscriptions.”
Founder @opendevsociety
Repository: Open-Dev-Society/OpenStock
Branch: main
Commit: 08304b51a449
Files: 97
Total size: 442.2 KB
Directory structure:
gitextract_k4znbyx3/
├── .github/
│ └── FUNDING.yml
├── .gitignore
├── .idea/
│ ├── .gitignore
│ ├── OpenStock.iml
│ ├── git_toolbox_prj.xml
│ ├── inspectionProfiles/
│ │ └── Project_Default.xml
│ ├── modules.xml
│ └── vcs.xml
├── API_DOCS.md
├── Dockerfile
├── LICENSE
├── README.md
├── app/
│ ├── (auth)/
│ │ ├── layout.tsx
│ │ ├── sign-in/
│ │ │ └── page.tsx
│ │ └── sign-up/
│ │ └── page.tsx
│ ├── (root)/
│ │ ├── about/
│ │ │ └── page.tsx
│ │ ├── api-docs/
│ │ │ └── page.tsx
│ │ ├── help/
│ │ │ └── page.tsx
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ │ ├── stocks/
│ │ │ └── [symbol]/
│ │ │ └── page.tsx
│ │ ├── terms/
│ │ │ └── page.tsx
│ │ └── watchlist/
│ │ └── page.tsx
│ ├── api/
│ │ └── inngest/
│ │ └── route.ts
│ ├── globals.css
│ └── layout.tsx
├── components/
│ ├── DonatePopup.tsx
│ ├── Footer.tsx
│ ├── Header.tsx
│ ├── NavItems.tsx
│ ├── OpenDevSocietyBranding.tsx
│ ├── SearchCommand.tsx
│ ├── SirayBanner.tsx
│ ├── TradingViewWidget.tsx
│ ├── UserDropdown.tsx
│ ├── WatchlistButton.tsx
│ ├── forms/
│ │ ├── CountrySelectField.tsx
│ │ ├── FooterLink.tsx
│ │ ├── InputField.tsx
│ │ └── SelectField.tsx
│ ├── ui/
│ │ ├── avatar.tsx
│ │ ├── button.tsx
│ │ ├── command.tsx
│ │ ├── dialog.tsx
│ │ ├── dropdown-menu.tsx
│ │ ├── input.tsx
│ │ ├── label.tsx
│ │ ├── popover.tsx
│ │ ├── select.tsx
│ │ └── sonner.tsx
│ └── watchlist/
│ ├── AlertsPanel.tsx
│ ├── CreateAlertModal.tsx
│ ├── NewsGrid.tsx
│ ├── TradingViewWatchlist.tsx
│ ├── WatchlistManager.tsx
│ ├── WatchlistStockChip.tsx
│ └── WatchlistTable.tsx
├── components.json
├── database/
│ ├── models/
│ │ ├── alert.model.ts
│ │ └── watchlist.model.ts
│ └── mongoose.ts
├── docker-compose.yml
├── eslint.config.mjs
├── hooks/
│ ├── useDebounce.ts
│ └── useTradingViewWidget.tsx
├── lib/
│ ├── actions/
│ │ ├── alert.actions.ts
│ │ ├── auth.actions.ts
│ │ ├── finnhub.actions.ts
│ │ ├── user.actions.ts
│ │ └── watchlist.actions.ts
│ ├── better-auth/
│ │ └── auth.ts
│ ├── constants.ts
│ ├── inngest/
│ │ ├── client.ts
│ │ ├── functions.ts
│ │ └── prompts.ts
│ ├── kit.ts
│ ├── nodemailer/
│ │ ├── index.ts
│ │ └── templates.ts
│ └── utils.ts
├── middleware/
│ └── index.ts
├── next.config.ts
├── package.json
├── postcss.config.mjs
├── scripts/
│ ├── check-env.mjs
│ ├── check_db_name.js
│ ├── create-kit-tag.mjs
│ ├── inspect-user.mjs
│ ├── list-kit-forms.mjs
│ ├── migrate-users-to-kit.mjs
│ ├── resolve_srv.js
│ ├── seed-inactive-user.mjs
│ ├── test-db.mjs
│ ├── test-db.ts
│ ├── test-kit.mjs
│ └── verify-watchlist.mjs
├── tsconfig.json
└── types/
└── global.d.ts
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms
github: [ravixalgorithm]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: ravixalgorithm
thanks_dev: # Replace with a single thanks.dev username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
================================================
FILE: .gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
================================================
FILE: .idea/.gitignore
================================================
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
================================================
FILE: .idea/OpenStock.iml
================================================
Modern. Open. Resilient.
© Open Dev Society. This project is licensed under AGPL-3.0; if you modify, redistribute, or deploy it (including as a web service), you must release your source code under the same license and credit the original authors.
“For me, OpenStock isn’t just another stock app. It’s about giving people clarity and control in the market, without barriers or subscriptions.”
Founder @opendevsociety
We believe financial intelligence shouldn't be locked behind paywalls. OpenStock is built by the community, for the community.
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.
{desc}
A transparent look at the event-driven, multi-provider system powering your market insights.
We prioritize uptime for generative features (Welcome Emails, News Summaries) using a robust multi-provider strategy. Our system automatically routes around outages.
Handles high-volume inference for news summarization and personalization.
Instant failover protection. If Gemini wavers, Siray takes over to ensure zero dropped requests.
{desc}
{desc}
Community-powered support for everyone.
{faq.answer}
Built on trust, transparency, and community values. No hidden gotchas, just clear rules.
Last updated: October 2025
**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.
Questions about these terms? Email us at opendevsociety@gmail.com
Track your favorite stocks and manage alerts.
{error.message}
}Helps us show market data and news relevant to you.
{text}{` `} {linkText}
{error.message}
}No stocks in watchlist.
)}Add stocks to track their performance and set alerts.
| Company | Symbol | Price | Change | Market Cap | Actions |
|---|---|---|---|---|---|
|
{stock.logo ? (
{stock.symbol[0]}
)}
{stock.name}
|
{stock.symbol} | {formatCurrency(stock.price)} |
{isPositive ?
|
{formatNumber(stock.marketCap)} |
|
No market news available today. Please check back tomorrow.
'; export const WATCHLIST_TABLE_HEADER = [ 'Company', 'Symbol', 'Price', 'Change', 'Market Cap', 'P/E Ratio', 'Alert', 'Action', ]; ================================================ FILE: lib/inngest/client.ts ================================================ import {Inngest} from "inngest" export const inngest = new Inngest({ id: "openStock", ai: {gemini: {apiKey: process.env.GEMINI_API_KEY}}, // Add signing key for Vercel deployment signingKey: process.env.INNGEST_SIGNING_KEY, }) ================================================ FILE: lib/inngest/functions.ts ================================================ import { inngest } from "@/lib/inngest/client"; import { NEWS_SUMMARY_EMAIL_PROMPT, PERSONALIZED_WELCOME_EMAIL_PROMPT } from "@/lib/inngest/prompts"; import { sendNewsSummaryEmail, sendWelcomeEmail } from "@/lib/nodemailer"; import { getAllUsersForNewsEmail } from "@/lib/actions/user.actions"; import { getWatchlistSymbolsByEmail } from "@/lib/actions/watchlist.actions"; import { getNews } from "@/lib/actions/finnhub.actions"; import { getFormattedTodayDate } from "@/lib/utils"; export const sendSignUpEmail = inngest.createFunction( { id: 'sign-up-email' }, { event: 'app/user.created' }, async ({ event, step }) => { const userProfile = ` - Country: ${event.data.country} - Investment goals: ${event.data.investmentGoals} - Risk tolerance: ${event.data.riskTolerance} - Preferred industry: ${event.data.preferredIndustry} ` const prompt = PERSONALIZED_WELCOME_EMAIL_PROMPT.replace('{{userProfile}}', userProfile) let aiResponse; try { aiResponse = await step.ai.infer('generate-welcome-intro', { model: step.ai.models.gemini({ model: 'gemini-2.5-flash-lite' }), body: { contents: [ { role: 'user', parts: [ { text: prompt } ] }] } }); } catch (error) { console.error("⚠️ Gemini API failed, switching to Siray.ai fallback", error); // Fallback Step aiResponse = await step.run('generate-welcome-intro-fallback', async () => { const SIRAY_API_KEY = process.env.SIRAY_API_KEY; if (!SIRAY_API_KEY) throw new Error("Siray API Key missing"); // Simulated OpenAI-compatible call const res = await fetch('https://api.siray.ai/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${SIRAY_API_KEY}` }, body: JSON.stringify({ model: 'siray-1.0-ultra', // Hypothetical model messages: [{ role: 'user', content: prompt }] }) }); if (!res.ok) throw new Error(`Siray API Error: ${res.statusText}`); const data = await res.json(); // Map to Gemini format for compatibility downstream return { candidates: [{ content: { parts: [{ text: data.choices[0].message.content }] } }] }; }); } await step.run('send-welcome-email', async () => { try { const part = aiResponse.candidates?.[0]?.content?.parts?.[0]; const introText = (part && 'text' in part ? part.text : null) || 'Thanks for joining Openstock. You now have the tools to track markets and make smarter moves.' const { data: { email, name } } = event; console.log(`📧 Attempting to send welcome email to: ${email}`); const result = await sendWelcomeEmail({ email, name, intro: introText }); console.log(`✅ Welcome email sent successfully to: ${email}`); return result; } catch (error) { console.error('❌ Error sending welcome email:', error); throw error; } }) return { success: true, message: 'Welcome email sent successfully' } } ) // Rename to Weekly export const sendWeeklyNewsSummary = inngest.createFunction( { id: 'weekly-news-summary' }, [{ event: 'app/send.weekly.news' }, { cron: '0 9 * * 1' }], // Every Monday at 9AM async ({ step }) => { // Step 1: Fetch General Market News const articles = await step.run('fetch-general-news', async () => { const { getNews } = await import("@/lib/actions/finnhub.actions"); const news = await getNews(); // Ideally getNews would accept range, but getting latest 10 is good for summary return (news || []).slice(0, 10); }); if (!articles || articles.length === 0) { return { message: 'No news available to summarize.' }; } // Doing AI step outside 'run' to use Inngest AI wrapper features properly const prompt = NEWS_SUMMARY_EMAIL_PROMPT.replace('{{newsData}}', JSON.stringify(articles, null, 2)) .replace('daily', 'weekly') .replace('Daily', 'Weekly'); let aiResponse; try { aiResponse = await step.ai.infer('generate-news-summary', { model: step.ai.models.gemini({ model: 'gemini-2.5-flash-lite' }), body: { contents: [{ role: 'user', parts: [{ text: prompt }] }] } }); } catch (error) { console.error("⚠️ Gemini API failed (Weekly News), switching to Siray.ai fallback", error); aiResponse = await step.run('generate-news-summary-fallback', async () => { const SIRAY_API_KEY = process.env.SIRAY_API_KEY; if (!SIRAY_API_KEY) return { candidates: [{ content: { parts: [{ text: "Market is moving. Log in to see more." }] } }] }; const res = await fetch('https://api.siray.ai/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${SIRAY_API_KEY}` }, body: JSON.stringify({ model: 'siray-1.0-ultra', messages: [{ role: 'user', content: prompt }] }) }); if (!res.ok) throw new Error("Siray API Error"); const data = await res.json(); return { candidates: [{ content: { parts: [{ text: data.choices[0].message.content }] } }] }; }); } const part = aiResponse.candidates?.[0]?.content?.parts?.[0]; const summaryText = (part && 'text' in part ? part.text : null) || 'Market is moving. Log in to see more.'; // Step 3: Send Broadcast via Kit await step.run('send-kit-broadcast', async () => { const { kit } = await import("@/lib/kit"); const { getFormattedTodayDate } = await import("@/lib/utils"); // Fetch subscribers for verification log try { const subData = await kit.listSubscribers(); const subscriberList = subData.subscribers || []; const confirmedCount = subscriberList.filter((s: any) => s.state === 'active').length; console.log(`📋 Target Audience: Found ${subData.total_subscribers} total subscribers in Kit.`); console.log(`✅ Confirmed (Active) Subscribers receiving email: ${confirmedCount}`); // Log names/emails for the user to see in Inngest dashboard if (subscriberList.length > 0) { console.log('--- Recipient List ---'); subscriberList.forEach((s: any) => { console.log(`${s.email_address} (${s.first_name || 'No Name'}) - Status: ${s.state}`); }); console.log('----------------------'); } } catch (e) { console.warn("Could not list subscribers for logging:", e); } const date = getFormattedTodayDate(); const subject = `📈 Weekly Market Summary - ${date}`; // --- HTML EMAIL TEMPLATE --- // Using inline styles for compatibility. Accent Color: Teal (#20c997) const logoUrl = "https://raw.githubusercontent.com/ravixalgorithm/OpenStock/main/public/assets/images/logo.png"; const content = `
📊 OpenStockWe Miss You, ${firstName}
Hi ${firstName}, Market UpdateMarkets 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!
Stay sharp, You received this because you are an OpenStock user. Unsubscribe |
content
- Write exactly TWO sentences (add one more sentence than current single sentence) - Keep total content between 35-50 words for readability - Use for key personalized elements (their goals, sectors, etc.) - DO NOT include "Here's what you can do right now:" as this is already in the template - Make every word count toward personalization - Second sentence should add helpful context or reinforce the personalization Example personalized outputs (showing obvious customization with TWO sentences):Thanks for joining Openstock! As someone focused on technology growth stocks, you'll love our real-time alerts for companies like the ones you're tracking. We'll help you spot opportunities before they become mainstream news.
Great to have you aboard! Perfect for your conservative retirement strategy — we'll help you monitor dividend stocks without overwhelming you with noise. You can finally track your portfolio progress with confidence and clarity.
You're all set! Since you're new to investing, we've designed simple tools to help you build confidence while learning the healthcare sector you're interested in. Our beginner-friendly alerts will guide you without the confusing jargon.
` export const NEWS_SUMMARY_EMAIL_PROMPT = `Generate HTML content for a market news summary email that will be inserted into the NEWS_SUMMARY_EMAIL_TEMPLATE at the {{newsContent}} placeholder. News data to summarize: {{newsData}} CRITICAL FORMATTING REQUIREMENTS: - Return ONLY clean HTML content with NO markdown, NO code blocks, NO backticks - Structure content with clear sections using proper HTML headings and paragraphs - Use these specific CSS classes and styles to match the email template: SECTION HEADINGS (for categories like "Market Highlights", "Top Movers", etc.):Content goes here
STOCK/COMPANY MENTIONS: Stock Symbol for ticker symbols Company Name for company names PERFORMANCE INDICATORS: Use 📈 for gains, 📉 for losses, 📊 for neutral/mixed NEWS ARTICLE STRUCTURE: For each individual news item within a section, use this structure: 1. Article container with visual styling and icon 2. Article title as a subheading 3. Key takeaways in bullet points (2-3 actionable insights) 4. "What this means" section for context 5. "Read more" link to the original article 6. Visual divider between articles ARTICLE CONTAINER: Wrap each article in a clean, simple container:💡 Bottom Line: Simple explanation of why this news matters to your money in everyday language.
💡 Bottom Line: If you own tech stocks, today was good for you. If you're thinking about investing, tech companies might be a smart choice right now.
💡 Bottom Line: Apple is making money in different ways (phones AND services), so it's a pretty safe stock to own even when the economy gets shaky.
|
|
|
|
|
|
If you see this, the API connection is working.
", public: true }; try { const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const data = await response.json(); console.log("👉 API Response Status:", response.status); console.log("👉 Full Response Body:", JSON.stringify(data, null, 2)); } catch (e) { console.error("❌ Request Failed:", e); } } runTest(); ================================================ FILE: scripts/verify-watchlist.mjs ================================================ import 'dotenv/config'; import mongoose from 'mongoose'; import { addToWatchlist, removeFromWatchlist, getUserWatchlist, isStockInWatchlist } from '../lib/actions/watchlist.actions.js'; import { createAlert, getUserAlerts } from '../lib/actions/alert.actions.js'; import { getWatchlistData } from '../lib/actions/finnhub.actions.js'; // Mock data const MOCK_USER_ID = 'verify-user-' + Date.now(); const SYMBOL = 'AAPL'; const COMPANY = 'Apple Inc'; // Monkey patch revalidatePath to avoid Next.js error in script global.fetch = fetch; // Ensure fetch is available import { jest } from '@jest/globals'; // Not using jest, just need to mock module if possible. // Actually, simple mock: const mockRevalidatePath = () => { }; // We can't easily mock module import in ESM without loader hooks. // But the actions import 'next/cache'. This script will fail if next/cache is not found or environment is not Next.js. // We might need to run this verification via a Next.js API route or just run the dev server and test manually? // Alternative: Creating a temporary test page or API route is safer for server actions. // OR: We comment out revalidatePath in actions for testing? No. // Let's try running it. If it fails on 'next/cache', we'll switch to manual verification. console.log('--- STARTING VERIFICATION ---'); // We will rely on manual verification for Server Actions mostly because they depend on Next.js context (headers, cache). // But we can test models and Finnhub actions. async function verifyFinnhub() { console.log('1. Testing Finnhub Quote...'); const data = await getWatchlistData([SYMBOL]); console.log('Finnhub Data:', data); if (data.length > 0 && data[0].price > 0) { console.log('✅ Finnhub Quote Fetch Success'); } else { console.error('❌ Finnhub Quote Fetch Failed'); } } async function verifyDB() { const uri = process.env.MONGODB_URI; await mongoose.connect(uri, { bufferCommands: false, family: 4 }); console.log('Connected to DB'); } // Just verifying Finnhub for now as it's the external dependency. // Database interactions are standard Mongoose. async function main() { await verifyFinnhub(); process.exit(0); } main(); ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "target": "ES2017", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, "noEmit": true, "esModuleInterop": true, "module": "esnext", "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", "incremental": true, "plugins": [ { "name": "next" } ], "paths": { "@/*": ["./*"] } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "exclude": ["node_modules"] } ================================================ FILE: types/global.d.ts ================================================ declare global { type SignInFormData = { email: string; password: string; }; type SignUpFormData = { fullName: string; email: string; password: string; country: string; investmentGoals: string; riskTolerance: string; preferredIndustry: string; }; type CountrySelectProps = { name: string; label: string; control: Control; error?: FieldError; required?: boolean; }; type FormInputProps = { name: string; label: string; placeholder: string; type?: string; register: UseFormRegister; error?: FieldError; validation?: RegisterOptions; disabled?: boolean; value?: string; }; type Option = { value: string; label: string; }; type SelectFieldProps = { name: string; label: string; placeholder: string; options: readonly Option[]; control: Control; error?: FieldError; required?: boolean; }; type FooterLinkProps = { text: string; linkText: string; href: string; }; type SearchCommandProps = { renderAs?: 'button' | 'text'; label?: string; initialStocks: StockWithWatchlistStatus[]; }; type WelcomeEmailData = { email: string; name: string; intro: string; }; type User = { id: string; name: string; email: string; }; type Stock = { symbol: string; name: string; exchange: string; type: string; }; type StockWithWatchlistStatus = Stock & { isInWatchlist: boolean; }; type FinnhubSearchResult = { symbol: string; description: string; displaySymbol?: string; type: string; }; type FinnhubSearchResponse = { count: number; result: FinnhubSearchResult[]; }; type StockDetailsPageProps = { params: Promise<{ symbol: string; }>; }; type WatchlistButtonProps = { symbol: string; company: string; isInWatchlist: boolean; showTrashIcon?: boolean; type?: 'button' | 'icon'; onWatchlistChange?: (symbol: string, isAdded: boolean) => void; }; type QuoteData = { c?: number; dp?: number; }; type ProfileData = { name?: string; marketCapitalization?: number; }; type FinancialsData = { metric?: { [key: string]: number }; }; type SelectedStock = { symbol: string; company: string; currentPrice?: number; }; type WatchlistTableProps = { watchlist: StockWithData[]; }; type StockWithData = { userId: string; symbol: string; company: string; addedAt: Date; currentPrice?: number; changePercent?: number; priceFormatted?: string; changeFormatted?: string; marketCap?: string; peRatio?: string; }; type AlertsListProps = { alertData: Alert[] | undefined; }; type MarketNewsArticle = { id: number; headline: string; summary: string; source: string; url: string; datetime: number; category: string; related: string; image?: string; }; type WatchlistNewsProps = { news?: MarketNewsArticle[]; }; type SearchCommandProps = { open?: boolean; setOpen?: (open: boolean) => void; renderAs?: 'button' | 'text'; buttonLabel?: string; buttonVariant?: 'primary' | 'secondary'; className?: string; }; type AlertData = { symbol: string; company: string; alertName: string; alertType: 'upper' | 'lower'; threshold: string; }; type AlertModalProps = { alertId?: string; alertData?: AlertData; action?: string; open: boolean; setOpen: (open: boolean) => void; }; type RawNewsArticle = { id: number; headline?: string; summary?: string; source?: string; url?: string; datetime?: number; image?: string; category?: string; related?: string; }; type Alert = { id: string; symbol: string; company: string; alertName: string; currentPrice: number; alertType: 'upper' | 'lower'; threshold: number; changePercent?: number; }; } export {};