Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
9ac82f9
Release: Merge main into production (#26)
Tamir26 Mar 14, 2026
d2f2bfe
Release: Merge main into production (#28)
Tamir26 Mar 14, 2026
b36db21
Resolve merge conflict in setup_runner.sh by using the latest version…
Tamir26 Mar 14, 2026
e0c266f
Fix: use 'docker compose' (v2) instead of legacy 'docker-compose'
Tamir26 Mar 14, 2026
570c5c6
Fix Docker permissions (sudo) and remove obsolete version key
Tamir26 Mar 14, 2026
1bb72ed
Fix hotfix sync: create PR instead of direct push (main is protected)
Tamir26 Mar 14, 2026
4f6afa4
Remove sudo from docker commands (node65 now in docker group)
Tamir26 Mar 14, 2026
28c28b3
Fix SPA fallback: allow /auth/success to reach React frontend
Tamir26 Mar 14, 2026
eccace6
Feature/UI and auth fixes (#33) (#34)
Tamir26 Mar 15, 2026
ba0264a
Resolve merge conflict in AuthSuccess.tsx - use refreshProfile from main
Tamir26 Mar 15, 2026
4d4e474
Fix profile image fallback, fix Layout syntax, add DD-MM-YY-HH-mm to …
Tamir26 Mar 15, 2026
db57ca9
Release: Merge main into production [17-03-26-10-07] (#40)
Tamir26 Mar 17, 2026
f406c51
Release: Merge main into production [17-03-26-10-20] (#42)
Tamir26 Mar 17, 2026
fb39061
Release: Merge main into production [17-03-26-13-37] (#44)
Tamir26 Mar 17, 2026
283aa41
Secure public user profiles by omitting emails and refresh tokens fro…
Tamir26 Mar 17, 2026
8a616a1
Release: Merge main into production [17-03-26-15-15] (#46)
Tamir26 Mar 17, 2026
06d5297
feat: improve user search intent and expose Swagger/Jest UI
Tamir26 Mar 17, 2026
79090d0
feat: add mock data seeding script
Tamir26 Mar 17, 2026
22ae424
Release: Merge main into production [17-03-26-17-58] (#48)
Tamir26 Mar 17, 2026
22fc8e9
feat: fix production seeding and UI document persistence
Tamir26 Mar 17, 2026
12bca59
feat: finalize production-standard project structure for professor re…
Tamir26 Mar 17, 2026
d5f0e39
chore: resolve merge conflicts with main
Tamir26 Mar 17, 2026
7058456
Merge remote-tracking branch 'origin/main' into feature/fix-ui-and-se…
Tamir26 Mar 17, 2026
32c0128
chore: resolve conflicts and merge feature fixes directly into produc…
Tamir26 Mar 17, 2026
ede2063
fix: update CSP to allow external images for mock data
Tamir26 Mar 17, 2026
652d2f6
fix: move swagger and test-report routes before SPA fallback
Tamir26 Mar 17, 2026
25b3ecd
fix: restore swagger endpoints in production and resolve test timeout…
Tamir26 Mar 17, 2026
ecce7f3
fix: ignore stale test reports and increase test setup timeouts
Tamir26 Mar 17, 2026
90e4a66
fix: secure and automate test report refresh in production
Tamir26 Mar 17, 2026
cc1a2ba
fix: remove stale local test report from repository
Tamir26 Mar 17, 2026
b2b4379
fix: run tests during docker build stage to generate report reliably
Tamir26 Mar 17, 2026
ec6a788
fix: relocate tests to host runner and increase timeouts to 60s for s…
Tamir26 Mar 17, 2026
907c702
fix: enable pending AI tests by ensuring ENV and secrets are passed t…
Tamir26 Mar 17, 2026
e89e8c6
fix: update AI tests to run if either Gemini or OpenRouter key is pre…
Tamir26 Mar 17, 2026
c132909
fix: make AI service key validation more flexible and add production …
Tamir26 Mar 17, 2026
2ffca1e
fix: enable experimental-vm-modules for AI test compatibility and swi…
Tamir26 Mar 17, 2026
2f4144e
fix: implement robust Avatar component and ensure horizontal navbar l…
Tamir26 Mar 17, 2026
0f191be
fix: restore missing imports and final UI layout tweaks
Tamir26 Mar 17, 2026
7018a9b
refactor: split deployment into test and deploy jobs, and improve ava…
Tamir26 Mar 17, 2026
5352a7b
fix: add missing useEffect import for Avatar resilience
Tamir26 Mar 17, 2026
fca6795
fix: make OAuth initialization optional and provide secrets to CI tes…
Tamir26 Mar 17, 2026
279e3c3
fix: include Google OAuth secrets in test job environment
Tamir26 Mar 17, 2026
461bc8f
feat: implement professional split-stage pipeline, OAuth resilience, …
Tamir26 Mar 17, 2026
b5160f5
fix: migrate PostDetails and UserProfile to Avatar component
Tamir26 Mar 17, 2026
4ed797d
fix: refine navbar layout and hide profile dropdown caret
Tamir26 Mar 17, 2026
ab71976
fix: make alert action icons visible using css color
Tamir26 Mar 18, 2026
dfe8694
fix: add text labels to alert actions to ensure visibility
Tamir26 Mar 18, 2026
d3fa249
fix: resolve alert persistence and trigger logic for CVX and other st…
Tamir26 Mar 19, 2026
1c70a4b
fix: isolate test env from .env variables and rename CI job
Tamir26 Mar 19, 2026
cf41670
fix: resolve AI test environment pollution by removing hardcoded dote…
Tamir26 Mar 19, 2026
4a326c2
fix: resolve LlamaIndex dynamic import error in tests by enabling exp…
Tamir26 Mar 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
workflow_dispatch:

jobs:
build-and-test:
test-and-build:
runs-on: ubuntu-latest

services:
Expand All @@ -30,7 +30,7 @@ jobs:
npm ci --prefix client

- name: Run tests
run: npm test
run: NODE_OPTIONS="--experimental-vm-modules" npm test
env:
MONGO_URI: mongodb://localhost:27017/test_db
JWT_SECRET: ${{ secrets.JWT_SECRET }}
Expand All @@ -41,6 +41,7 @@ jobs:
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
BCRYPT_SALT_ROUNDS: 1

- name: Build (Backend & Frontend)
run: npm run build
61 changes: 55 additions & 6 deletions .github/workflows/deploy-prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,63 @@ on:
workflow_dispatch:

jobs:
deploy:
run-tests:
runs-on: self-hosted

steps:
- uses: actions/checkout@v4

- name: Set up Environment
run: |
echo "PORT=${{ vars.PORT || 3000 }}" > .env
echo "NODE_ENV=test" >> .env
echo "MONGO_URI=mongodb://admin:market_watch_pass@127.0.0.1:27017/test_db?authSource=admin" >> .env
echo "JWT_SECRET=${{ secrets.JWT_SECRET }}" >> .env
echo "REFRESH_TOKEN_SECRET=${{ secrets.REFRESH_TOKEN_SECRET }}" >> .env
echo "OPENROUTER_API_KEY=${{ secrets.OPENROUTER_API_KEY }}" >> .env
echo "GEMINI_API_KEY=${{ secrets.GEMINI_API_KEY }}" >> .env
echo "GOOGLE_CLIENT_ID=${{ secrets.GOOGLE_CLIENT_ID }}" >> .env
echo "GOOGLE_CLIENT_SECRET=${{ secrets.GOOGLE_CLIENT_SECRET }}" >> .env

- name: Use Node.js 22
uses: actions/setup-node@v4
with:
node-version: 22

- name: Run Tests on Host
run: |
npm install
docker compose up -d mongodb
sleep 15
MONGO_URI=mongodb://admin:market_watch_pass@127.0.0.1:27017/test_db?authSource=admin \
GEMINI_API_KEY=${{ secrets.GEMINI_API_KEY }} \
OPENROUTER_API_KEY=${{ secrets.OPENROUTER_API_KEY }} \
JWT_SECRET=${{ secrets.JWT_SECRET }} \
REFRESH_TOKEN_SECRET=${{ secrets.REFRESH_TOKEN_SECRET }} \
NODE_OPTIONS=--experimental-vm-modules \
BCRYPT_SALT_ROUNDS=1 npm test
docker compose stop mongodb

- name: Upload Test Report
if: always()
uses: actions/upload-artifact@v4
with:
name: test-report
path: test-report.html
retention-days: 1

deploy-app:
needs: run-tests
runs-on: self-hosted
steps:
- uses: actions/checkout@v4

- name: Download Test Report
uses: actions/download-artifact@v4
with:
name: test-report
path: .

- name: Set up Production Environment
run: |
echo "PORT=${{ vars.PORT || 3000 }}" > .env
echo "NODE_ENV=${{ vars.NODE_ENV || 'production' }}" >> .env
Expand All @@ -27,9 +77,8 @@ jobs:
echo "GOOGLE_CLIENT_ID=${{ secrets.GOOGLE_CLIENT_ID }}" >> .env
echo "GOOGLE_CLIENT_SECRET=${{ secrets.GOOGLE_CLIENT_SECRET }}" >> .env


- name: Build and Restart with Docker Compose
- name: Build and Restart App Container
run: |
docker-compose down
docker-compose up --build -d
docker compose down
docker compose up --build -d
docker system prune -f
31 changes: 22 additions & 9 deletions .github/workflows/sync-main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ on:
branches: [ "production" ]
workflow_dispatch:

permissions:
contents: write
pull-requests: write

jobs:
sync-to-main:
runs-on: ubuntu-latest
# ONLY run if the push to production did NOT come from main
# This avoids the "Main -> Prod -> Main" circular loop
if: github.event.head_commit.author.name != 'github-actions' && !contains(github.event.head_commit.message, 'main')

steps:
Expand All @@ -18,15 +21,25 @@ jobs:
fetch-depth: 0
token: ${{ secrets.PR_TOKEN || secrets.GITHUB_TOKEN }}

- name: Sync changes to main
- name: Create PR to sync production back to main
env:
GH_TOKEN: ${{ secrets.PR_TOKEN || secrets.GITHUB_TOKEN }}
run: |
git config --global user.name 'github-actions[bot]'
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
git fetch origin main production

git checkout main
git fetch origin production
# Check if there are differences
if git diff --quiet origin/main..origin/production; then
echo "No differences between production and main. Skipping."
exit 0
fi

# Use allow-unrelated-histories to bridge the gap between branches if they diverged
git merge origin/production --allow-unrelated-histories -m "🤖 Sync hotfix from production to main"
# Create a sync branch from production
git checkout -b sync/prod-to-main origin/production

git push origin main
# Create PR using GitHub CLI
gh pr create \
--base main \
--head sync/prod-to-main \
--title "🤖 Sync: Production hotfixes back to main" \
--body "Automated PR to sync hotfix changes from production back to main." \
|| echo "PR already exists or nothing to sync."
7 changes: 2 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,12 @@ RUN npm install --omit=dev
# Copy compiled backend
COPY --from=build /app/dist ./dist

# Copy test report (generated during build test step if added to workflow, or copied if exists)
COPY --from=build /app/test-report.html ./test-report.html
# Copy test report (generated by GitHub Actions Host Runner)
COPY ./test-report.html ./test-report.html

# Copy compiled frontend
COPY --from=build /app/client/dist ./client/dist

# Copy test report (if generated)
COPY --from=build /app/test-report.html ./test-report.html

# Copy other necessary files (like uploads or assets)
COPY src/uploads ./src/uploads

Expand Down
Binary file added ai_test_err.txt
Binary file not shown.
Binary file added all_tests_out.txt
Binary file not shown.
81 changes: 81 additions & 0 deletions client/src/components/Avatar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { useState, useEffect } from 'react';
import { Image } from 'react-bootstrap';
import { User } from 'lucide-react';
import { getImageUrl } from '../services/api';

interface AvatarProps {
src?: string | null;
username?: string;
size?: number;
className?: string;
border?: boolean;
}

const Avatar = ({ src, username, size = 40, className = '', border = false }: AvatarProps) => {
const [error, setError] = useState(false);

// Reset error when source changes
useEffect(() => {
setError(false);
}, [src]);

// Reset error if src changes
const hasImage = src &&
src !== 'null' &&
src !== 'undefined' &&
src !== '' &&
!src.includes('undefined') &&
!src.includes('null');

if (hasImage && !error) {
return (
<div
className={`rounded-circle overflow-hidden d-flex align-items-center justify-content-center ${className} ${border ? 'border' : ''}`}
style={{ width: `${size}px`, height: `${size}px` }}
>
<Image
src={getImageUrl(src)}
width={size}
height={size}
style={{ objectFit: 'cover', width: '100%', height: '100%' }}
onError={() => setError(true)}
alt={username || 'User avatar'}
/>
</div>
);
}

// Fallback: Initials or Icon with premium dynamic colors
const initials = username ? username[0].toUpperCase() : null;

// Generate a consistent color based on username
const colors = [
{ bg: 'rgba(13, 110, 253, 0.1)', text: '#0d6efd' }, // Blue
{ bg: 'rgba(102, 16, 242, 0.1)', text: '#6610f2' }, // Indigo
{ bg: 'rgba(111, 66, 193, 0.1)', text: '#6f42c1' }, // Purple
{ bg: 'rgba(214, 51, 132, 0.1)', text: '#d63384' }, // Pink
{ bg: 'rgba(253, 126, 20, 0.1)', text: '#fd7e14' }, // Orange
{ bg: 'rgba(25, 135, 84, 0.1)', text: '#198754' } // Green
];

const colorIndex = username ? username.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0) % colors.length : 0;
const selectedColor = colors[colorIndex];

return (
<div
className={`rounded-circle d-flex align-items-center justify-content-center ${className} ${border ? 'border' : ''}`}
style={{
width: `${size}px`,
height: `${size}px`,
backgroundColor: initials ? selectedColor.bg : '#f2f2f2',
color: initials ? selectedColor.text : '#6c757d',
fontSize: `${size * 0.4}px`,
fontWeight: 600
}}
>
{initials ? initials : <User size={size * 0.6} />}
</div>
);
};

export default Avatar;
34 changes: 4 additions & 30 deletions client/src/components/CommentSection.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { useState, useEffect, type FormEvent } from 'react';
import { Card, Button, Form, Image, Spinner, Dropdown } from 'react-bootstrap';
import { Card, Button, Form, Spinner, Dropdown } from 'react-bootstrap';
import { useAuth } from '../context/AuthContext';
import { getComments, createComment, deleteComment, updateComment, type Comment } from '../services/commentService';
import { Send, MoreVertical, Trash2, Edit2, X, Check } from 'lucide-react';
import { getImageUrl } from '../services/api';
import Avatar from './Avatar';

interface CommentSectionProps {
postId: string;
Expand Down Expand Up @@ -104,20 +104,7 @@ const CommentSection = ({ postId }: CommentSectionProps) => {
<Card className="border-0 shadow-sm mb-4 rounded-4">
<Card.Body className="p-3">
<Form onSubmit={handleSubmit} className="d-flex gap-2">
{user?.image ? (
<Image
src={getImageUrl(user.image)}
roundedCircle
width={32}
height={32}
className="border flex-shrink-0"
style={{ objectFit: 'cover' }}
/>
) : (
<div className="bg-light rounded-circle d-flex align-items-center justify-content-center border flex-shrink-0" style={{ width: '32px', height: '32px' }}>
<span className="text-muted small">{user?.username?.[0] || 'U'}</span>
</div>
)}
<Avatar src={user?.image} username={user?.username} size={32} border />
<Form.Control
type="text"
placeholder="Write a comment..."
Expand Down Expand Up @@ -149,20 +136,7 @@ const CommentSection = ({ postId }: CommentSectionProps) => {
<div className="d-flex flex-column gap-3">
{comments.map(comment => (
<div key={comment._id} className="d-flex gap-2">
{comment.owner?.image ? (
<Image
src={getImageUrl(comment.owner.image)}
roundedCircle
width={32}
height={32}
className="border flex-shrink-0 mt-1"
style={{ objectFit: 'cover' }}
/>
) : (
<div className="bg-primary bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center border flex-shrink-0 mt-1" style={{ width: '32px', height: '32px' }}>
<span className="text-primary fw-bold small">{comment.owner?.username?.[0]?.toUpperCase() || 'U'}</span>
</div>
)}
<Avatar src={comment.owner?.image} username={comment.owner?.username} size={32} className="mt-1" border />
<div className="flex-grow-1">
<div className="bg-white p-3 rounded-4 shadow-sm position-relative group">
<div className="d-flex justify-content-between align-items-start mb-1">
Expand Down
52 changes: 14 additions & 38 deletions client/src/components/Layout.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,9 @@
import { useState } from 'react';
import { Outlet, Link, useNavigate, useLocation } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
import { Container, Navbar, Nav, Dropdown, Image } from 'react-bootstrap';
import { LogOut, Search, PlusCircle, Home, TrendingUp, User, Bell } from 'lucide-react';
import { getImageUrl } from '../services/api';
import { Container, Navbar, Nav, Dropdown } from 'react-bootstrap';
import { Bell, Home, LogOut, PlusCircle, Search, TrendingUp } from 'lucide-react';

const ProfileAvatar = ({ user }: { user: any }) => {
const [imageError, setImageError] = useState(false);

// Check if the image string is valid
const hasValidImageStr = user?.image && user.image !== 'null' && user.image !== 'undefined';

if (hasValidImageStr && !imageError) {
return (
<Image
src={getImageUrl(user.image)}
roundedCircle
width={40}
height={40}
className="border"
style={{ objectFit: 'cover' }}
referrerPolicy="no-referrer"
onError={() => setImageError(true)}
/>
);
}

// Fallback UI
return (
<div className="bg-light rounded-circle d-flex align-items-center justify-content-center border" style={{ width: '40px', height: '40px' }}>
<User size={24} className="text-muted" />
</div>
);
};
import Avatar from './Avatar';

const Layout = () => {
const { user, logout } = useAuth();
Expand Down Expand Up @@ -85,18 +56,23 @@ const Layout = () => {
</Link>
</Nav>
)}
<Nav className="d-flex flex-row align-items-center justify-content-center gap-3 mt-3 mt-lg-0">
<Nav className="ms-auto d-flex align-items-center flex-nowrap gap-2 mt-3 mt-lg-0">
<Link
to="/create-post"
className="btn btn-light d-flex align-items-center justify-content-center p-2 rounded-circle border-0"
style={{ width: '40px', height: '40px', background: 'rgba(255,255,255,0.2)' }}
className="nav-link p-1 rounded-circle d-flex align-items-center justify-content-center border-0 hover-bg"
style={{ width: '40px', height: '40px' }}
title="Add Post"
>
<PlusCircle size={20} className="text-dark" />
<PlusCircle size={22} className="text-muted" />
</Link>

<Dropdown align="end">
<Dropdown.Toggle variant="transparent" className="p-0 border-0 no-arrow after-none">
<ProfileAvatar user={user} />
<Dropdown.Toggle
variant="link"
className="p-0 border-0 shadow-none text-decoration-none dropdown-toggle-no-caret d-flex align-items-center"
id="profile-dropdown"
>
<Avatar src={user?.image} username={user?.username} size={40} border />
</Dropdown.Toggle>

<Dropdown.Menu className="shadow border-0 rounded-4 mt-2 p-2">
Expand Down
Loading
Loading