diff --git a/README.md b/README.md index 233784c..7e67cf4 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,14 @@ **Understand job requirements, tailor your resume, and keep your applications organized.** -[![Live Demo](https://img.shields.io/badge/Try%20the%20App-JobHelperGuru-0a66c2?style=for-the-badge)](https://jobhelperguru.onrender.com/) +[![Live Demo](https://img.shields.io/badge/Try%20the%20App-JobHelperGuru-0a66c2?style=for-the-badge)](https://jobhelperguru.vercel.app/) [![CI/CD Pipeline](https://github.com/souravC01/JobHelperGuru/actions/workflows/ci.yml/badge.svg)](https://github.com/souravC01/JobHelperGuru/actions/workflows/ci.yml) JobHelperGuru brings job analysis, resume matching, writing assistance, and application tracking into one workspace. Start with a job link or pasted description, compare your resumes against its requirements, draft tailored bullets and outreach, then track the application through to an outcome. Use the built-in heuristic engine without an AI key, connect your own compatible AI provider, or run a local model with Ollama. -[Explore the live app](https://jobhelperguru.onrender.com/) · [Run locally](#run-locally) · [Development](#development-and-tests) · [Deployment](#deployment) +[Explore the live app](https://jobhelperguru.vercel.app/) · [Run locally](#run-locally) · [Development](#development-and-tests) · [Deployment](#deployment) ![Live analysis of Amazon's Software Development Engineer, Early Career 2026 role, showing categorized requirements and ATS keywords](docs/images/job-analysis.jpg) @@ -28,7 +28,7 @@ Use the built-in heuristic engine without an AI key, connect your own compatible | **Excel export** | Download a styled two-sheet workbook with application details, skills, status colors, and supported job hyperlinks. Untrusted cell values are written as text. | | **Provider profiles** | Manage your own AI connections using server-side encrypted keys and masked profile metadata. | -The application pipeline covers **Wishlist → Applied → Interviewing → Offered → Rejected → Archived**. Follow-up reminders and dashboard totals stay connected to tracker changes. +The application pipeline covers **Wishlist → Applied → Interviewing → Offered → Rejected → Archived**. Dashboard totals act as quick filters, and the follow-up filter shows applications with scheduled or overdue follow-ups.
See resume matching in action @@ -193,15 +193,14 @@ Inspect the dry-run counts and unowned records, and back up both database and re ## Deployment -The multi-stage [Dockerfile](Dockerfile) builds the React frontend and serves it with FastAPI/Uvicorn. The [Render blueprint](render.yaml) uses `/api/health` for health checks and disables Render's automatic deployment. +The public frontend is hosted on Vercel at **[jobhelperguru.vercel.app](https://jobhelperguru.vercel.app/)**. Vercel routes `/api` requests to the FastAPI backend running on Google Cloud Run. -The GitHub Actions workflow can trigger a Render deployment after all required jobs pass on a push to `main`, when the repository secret `RENDER_DEPLOY_HOOK_URL` is configured. Container checks do not publish an image. +The [GitHub Actions workflow](.github/workflows/ci.yml) runs backend tests, frontend tests and build, and both Docker build checks. When backend files change on `main`, a successful workflow builds and deploys the backend image to the existing Cloud Run service, then verifies the production health endpoint through Vercel. Frontend deployment remains managed by Vercel. For hosted persistence, configure PostgreSQL and R2 rather than relying on an ephemeral container filesystem. Set production mode, secrets, origins, public URL, and any Google sign-in settings before releasing. -- [Render deployment guide](docs/RENDER_DEPLOYMENT_GUIDE.md) +- [Cloud Run deployment and recovery guide](docs/cloud-run-cicd.md) - [Production environment template](.env.production.example) -- [Remediation validation record](docs/REMEDIATION_VALIDATION.md) ## Built with @@ -211,7 +210,7 @@ For hosted persistence, configure PostgreSQL and R2 rather than relying on an ep | API and authentication | FastAPI, Pydantic, JWT, bcrypt, Google Identity Services | | Storage | PostgreSQL / SQLite, Cloudflare R2 / local files, Fernet encryption | | Analysis and documents | OpenAI SDK, heuristic skill matching, Beautiful Soup, Trafilatura, pypdf, python-docx, openpyxl | -| Testing and delivery | pytest, Vitest, React Testing Library, GitHub Actions, Docker, Render | +| Testing and delivery | pytest, Vitest, React Testing Library, GitHub Actions, Docker, Vercel, Google Cloud Run | ## Project layout diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 5ea779b..f9dc7be 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -269,9 +269,6 @@ export default function App() {
JobHelperGuru - - Cloud -

AI Job Tailoring, Best-Fit Resumes & Personal Pipeline @@ -447,7 +444,7 @@ export default function App() {

- Targeting high-match roles accelerates interview conversion + Analyze a role. Tailor your resume. Track your progress.
@@ -489,6 +486,14 @@ export default function App() { refreshKey={rankingRefreshKey} onAdoptSkills={handleAdoptSkills} onRemoveAdoptedSkill={handleRemoveAdoptedSkill} + onOpenBulletOptimizer={(skills) => { + if (!currentUser) { + setAuthMode('login'); + setIsAuthOpen(true); + return; + } + handleOpenOptimizer(skills, selectedResumeForJob || resumes[0] || null); + }} onSelectKeywordForOptimization={(skills, rank, sectionType) => { if (!currentUser) { setAuthMode('login'); diff --git a/frontend/src/components/ApplicationsTracker.jsx b/frontend/src/components/ApplicationsTracker.jsx index f621e9f..2aafa0b 100644 --- a/frontend/src/components/ApplicationsTracker.jsx +++ b/frontend/src/components/ApplicationsTracker.jsx @@ -14,7 +14,6 @@ import { CheckCircle2, } from 'lucide-react'; import KanbanBoard from './KanbanBoard'; -import FollowUpBanner from './FollowUpBanner'; import { updateApplication, deleteApplication, @@ -23,6 +22,7 @@ import { import { localDate } from '../utils/localDate'; const STATUS_OPTIONS = ['Wishlist', 'Applied', 'Interviewing', 'Offered', 'Rejected', 'Archived']; +const METRIC_CARD_CLASS = 'card-corporate p-4 bg-white border border-[#e0e0e0] rounded-lg flex items-center justify-between shadow-none text-left cursor-pointer outline-offset-2 focus-visible:outline-2 focus-visible:outline-[#0a66c2] aria-pressed:outline-2 aria-pressed:outline-[#0a66c2]'; export default function ApplicationsTracker({ applications = [], @@ -31,9 +31,20 @@ export default function ApplicationsTracker({ const [viewMode, setViewMode] = useState('table'); // 'table' or 'kanban' const [searchQuery, setSearchQuery] = useState(''); const [statusFilter, setStatusFilter] = useState('ALL'); + const [followUpFilter, setFollowUpFilter] = useState('ALL'); // 'ALL', 'DUE', or 'SET' const [editingNotesId, setEditingNotesId] = useState(null); const [tempNotes, setTempNotes] = useState(''); + const selectSummaryFilter = (status = 'ALL', followUp = 'ALL') => { + setStatusFilter(status); + setFollowUpFilter(followUp); + setSearchQuery(''); + }; + + const isSummarySelected = (status = 'ALL', followUp = 'ALL') => ( + statusFilter === status && followUpFilter === followUp && searchQuery === '' + ); + const handleStatusChange = async (appId, newStatus) => { try { const updates = { status: newStatus }; @@ -89,6 +100,11 @@ export default function ApplicationsTracker({ const followUpCount = applications.filter((a) => a.follow_up_date).length; const todayStr = localDate(); + const isFollowUpDue = (app) => Boolean( + app.follow_up_date && app.follow_up_date <= todayStr && + !['Rejected', 'Archived', 'Offered'].includes(app.status) + ); + const dueFollowUpCount = applications.filter(isFollowUpDue).length; // Filtered applications const filteredApps = applications.filter((app) => { @@ -100,14 +116,22 @@ export default function ApplicationsTracker({ app.role.toLowerCase().includes(query) || (app.location && app.location.toLowerCase().includes(query)) || (app.required_skills && app.required_skills.some((s) => s.toLowerCase().includes(query))); - return matchesStatus && matchesSearch; + const matchesFollowUp = followUpFilter === 'DUE' + ? isFollowUpDue(app) + : followUpFilter === 'SET' ? Boolean(app.follow_up_date) : true; + return matchesStatus && matchesSearch && matchesFollowUp; }); return (
{/* 4 KPI Metrics Strip */}
-
+ -
+ -
+ -
+
- {/* Reminders Banner */} - - {/* Action & Filter Bar */}
@@ -204,11 +240,11 @@ export default function ApplicationsTracker({
{/* Filter Pills Bar + Search */} -
+
{/* Status Filter Pills */} -
+
+ + {followUpFilter === 'SET' && ( + + )} {STATUS_OPTIONS.map((st) => { const count = applications.filter((a) => a.status === st).length; return ( @@ -236,15 +297,15 @@ export default function ApplicationsTracker({
{/* Search Box */} -
+
setSearchQuery(e.target.value)} - className="input-corporate w-full pl-8 text-xs py-1.5" + className="input-corporate input-with-leading-icon w-full text-xs py-1.5" /> - +
@@ -258,6 +319,10 @@ export default function ApplicationsTracker({ Analyze a job description link above and click "Add to Pipeline" to populate your tracker!

+ ) : filteredApps.length === 0 ? ( +
+ No applications match your filters. +
) : viewMode === 'kanban' ? (
- +
- - - - - - - + + + + + + + {filteredApps.map((app) => { - const isOverdue = app.follow_up_date && app.follow_up_date <= todayStr; + const isOverdue = isFollowUpDue(app); return (
- {app.company} + {app.company} {app.url && ( )}
-
{app.role}
+
{app.role}
{app.required_skills && app.required_skills.length > 0 && (
{app.required_skills.slice(0, 3).map((s, i) => ( - + {s} ))} @@ -326,7 +391,7 @@ export default function ApplicationsTracker({
DateCompany & RoleStatusLocation & SalaryFollow-UpNotesActionsDateCompany & RoleStatusLocation & SalaryFollow-UpNotesActions
+
{app.location || 'Unknown'}
{app.salary && app.salary !== 'Not specified' && (
{app.salary}
@@ -351,7 +416,7 @@ export default function ApplicationsTracker({ type="date" value={app.follow_up_date || ''} onChange={(e) => handleFollowUpDateChange(app.id, e.target.value)} - className={`input-corporate px-2 py-1 text-xs font-mono ${ + className={`input-corporate w-full min-w-0 px-2 py-1 text-xs font-mono ${ isOverdue ? 'border-[#b24020] text-[#b24020] font-bold bg-[#b24020]/5' : '' }`} /> @@ -366,7 +431,7 @@ export default function ApplicationsTracker({ type="text" value={tempNotes} onChange={(e) => setTempNotes(e.target.value)} - className="input-corporate text-xs py-1" + className="input-corporate w-full min-w-0 text-xs py-1" autoFocus /> - ))} - - - ); -} diff --git a/frontend/src/components/JobAnalyzer.jsx b/frontend/src/components/JobAnalyzer.jsx index 9a16ed5..03f81f5 100644 --- a/frontend/src/components/JobAnalyzer.jsx +++ b/frontend/src/components/JobAnalyzer.jsx @@ -8,7 +8,6 @@ import { Briefcase, ExternalLink, PlusCircle, - Wand2, Mail, Loader2, AlertCircle, @@ -31,7 +30,6 @@ export default function JobAnalyzer({ onApplicationSaved, onAiError, }) { - const [mode, setMode] = useState('url'); // 'url' or 'text' const [urlInput, setUrlInput] = useState(''); const [textInput, setTextInput] = useState(''); const [loading, setLoading] = useState(false); @@ -64,10 +62,11 @@ export default function JobAnalyzer({ const handleAnalyze = async (e) => { e?.preventDefault(); + if (loading) return; setError(''); setSaveSuccess(false); - const activeMode = showPasteText ? 'text' : mode; + const activeMode = showPasteText ? 'text' : 'url'; if (activeMode === 'url' && !urlInput.trim()) { setError('Please enter a valid job URL.'); @@ -135,8 +134,8 @@ export default function JobAnalyzer({ placeholder="Paste LinkedIn, Greenhouse, Workday, Dayforce, or Lever job URL..." value={urlInput} onChange={(e) => setUrlInput(e.target.value)} - disabled={loading} - className="input-corporate w-full text-sm text-[#000000] placeholder:text-[#666666] h-11" + disabled={loading || showPasteText} + className="input-corporate w-full text-sm text-[#000000] placeholder:text-[#666666] h-11 disabled:opacity-50 disabled:cursor-not-allowed" /> {matchingAppByUrl && (
@@ -150,10 +149,10 @@ export default function JobAnalyzer({