Phase 1 Foundation: FM Portal — AI-Enabled Staffing & Onboarding#225
Phase 1 Foundation: FM Portal — AI-Enabled Staffing & Onboarding#225devin-ai-integration[bot] wants to merge 7 commits into
Conversation
Complete Phase 1 implementation including: Backend (FastAPI + Python): - 15+ SQLAlchemy models with full relationships - Pydantic schemas for all entities - CRUD API endpoints for all core entities - Dashboard, AI, Auth, SLA endpoints - AI service abstraction (Claude via Anthropic API + Amazon Bedrock) - Semantic search, candidate matching, resume parsing - Role rubric management with HTML file upload - SLA management with XLS/XLSX file attachment support - Practice Unit and Unit Anchor management - Auth service with Infosys SSO placeholder - WMT integration placeholder - Alembic migration setup - Seed data script (5 locations, 6 practice units, sample users) - Docker setup Frontend (React 19 + TypeScript + Material UI): - Dashboard with stat cards, SLA status, rotation tracking - Staffing Requests, Candidates, Roles pages - Practice Units management - Rubrics page with HTML upload and real-time preview - SLA Management page with XLS upload - AI Semantic Search page - Responsive sidebar layout - API service layer with typed endpoints - Vite build setup with API proxy
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
…erability - StaffingRequestsPage: Add missing 'title' field, rename headcount to number_of_positions - CandidatesPage: Use first_name/last_name instead of full_name, years_of_experience - RubricsPage: Replace dangerouslySetInnerHTML with sandboxed iframe for XSS protection - rubrics.py: Add Content-Security-Policy header to HTML endpoint - seed_data.py: Fix Location fields (name, location_type), fix User/PersonaType usage - types/index.ts: Update interfaces to match backend response schemas
…d work compliance, 24-month rotation tracker, OCP rubric, Admin button - Rename to FM Portal throughout - Fannie Mae color scheme (#00263e navy, #00838f teal, #e87722 orange) - Source Sans Pro font via Google Fonts - Wave-Based Staffing Plan: 4 waves (70+180+250+200=700) over 12 months - Hybrid Work Compliance section (3 days/week in-office) - 24-Month Rotation Tracker with due date chips - OCP Hosting rubric embedded in RubricsPage with preview - Admin button in top bar for future configuration - All 8 pages rewritten with clean, lightweight design - 5 locations (3 company, 2 client) with type labels - 6 practice units (ADM, CIS, QES, ARC, DAA, CSC)
| file_path = FRONTEND_DIST / full_path | ||
| if file_path.exists() and file_path.is_file(): | ||
| return FileResponse(str(file_path)) |
There was a problem hiding this comment.
🔴 Path traversal vulnerability in SPA catch-all route allows reading arbitrary server files
The serve_spa handler constructs a file path by joining the user-supplied full_path directly to FRONTEND_DIST without validating the resolved path stays within the intended directory. If full_path contains .. sequences (e.g., ../../etc/passwd), Path.__truediv__ does not sanitize them, and file_path.exists() resolves the .. segments, potentially serving files outside FRONTEND_DIST.
Path traversal demonstration
FRONTEND_DIST = Path('/app/frontend/dist')
full_path = '../../etc/passwd'
file_path = FRONTEND_DIST / full_path
# file_path = /app/frontend/dist/../../etc/passwd
# file_path.resolve() = /app/etc/passwd
# file_path.exists() checks the resolved pathThe code should verify file_path.resolve().is_relative_to(FRONTEND_DIST.resolve()) before serving.
| file_path = FRONTEND_DIST / full_path | |
| if file_path.exists() and file_path.is_file(): | |
| return FileResponse(str(file_path)) | |
| file_path = (FRONTEND_DIST / full_path).resolve() | |
| if file_path.is_relative_to(FRONTEND_DIST.resolve()) and file_path.exists() and file_path.is_file(): | |
| return FileResponse(str(file_path)) |
Was this helpful? React with 👍 or 👎 to provide feedback.
| interview_round: Mapped["InterviewRound"] = relationship(back_populates="feedbacks") | ||
| interviewer: Mapped["User"] = relationship() |
There was a problem hiding this comment.
🔴 Missing rubric_ratings relationship on InterviewFeedback model causes incomplete API responses
The InterviewFeedback SQLAlchemy model (backend/app/models/interview.py:58-77) has no rubric_ratings relationship, but the InterviewFeedbackResponse Pydantic schema (backend/app/schemas/interview.py:50) expects rubric_ratings: list[RubricRatingResponse]. While RubricRating has a forward relationship to InterviewFeedback (backend/app/models/rubric.py:72), there's no reverse back_populates on InterviewFeedback. This means the submit_feedback endpoint (backend/app/api/v1/endpoints/interviews.py:60-84) successfully creates rubric ratings in the DB but always returns them as an empty list [] in the response, since Pydantic falls back to the field default when the attribute is not found on the SQLAlchemy object. The same issue affects any GET endpoint that serializes InterviewFeedbackResponse.
| interview_round: Mapped["InterviewRound"] = relationship(back_populates="feedbacks") | |
| interviewer: Mapped["User"] = relationship() | |
| interview_round: Mapped["InterviewRound"] = relationship(back_populates="feedbacks") | |
| interviewer: Mapped["User"] = relationship() | |
| rubric_ratings: Mapped[list["RubricRating"]] = relationship(back_populates="interview_feedback", cascade="all, delete-orphan") |
Was this helpful? React with 👍 or 👎 to provide feedback.
… Candidate Identification Effectiveness
| if not file.filename or not file.filename.endswith((".html", ".htm")): | ||
| raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Only .html files are accepted") | ||
| content = await file.read() | ||
| rubric.html_content = content.decode("utf-8") |
There was a problem hiding this comment.
🟡 Unhandled UnicodeDecodeError when uploading non-UTF-8 HTML rubric file
In backend/app/api/v1/endpoints/rubrics.py:70, content.decode("utf-8") is called on the raw bytes of an uploaded file without error handling. If a user uploads an HTML file that is not valid UTF-8 (e.g., encoded in Latin-1 or contains corrupt bytes), this raises an unhandled UnicodeDecodeError, resulting in a 500 Internal Server Error instead of a user-friendly 400 Bad Request.
| rubric.html_content = content.decode("utf-8") | |
| try: | |
| rubric.html_content = content.decode("utf-8") | |
| except UnicodeDecodeError: | |
| raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="File must be valid UTF-8 encoded HTML") |
Was this helpful? React with 👍 or 👎 to provide feedback.
| ).scalar() or 0 | ||
| location_breakdown.append(LocationBreakdown( | ||
| location_name=f"{loc.city}, {loc.state}", | ||
| total_requests=total, fulfilled=fulfilled, in_progress=total - fulfilled, |
There was a problem hiding this comment.
🟡 Dashboard in_progress count incorrectly includes cancelled and on-hold requests
In backend/app/api/v1/endpoints/dashboard.py:55, the in_progress field is calculated as total - fulfilled. Since total counts ALL requests for a location (including those with status cancelled and on_hold), the in_progress count inflates the number by including requests that are no longer active. For example, if a location has 10 total requests (3 fulfilled, 2 cancelled, 5 active), in_progress would show 7 instead of the correct 5.
Prompt for agents
In backend/app/api/v1/endpoints/dashboard.py, the get_dashboard function computes in_progress as total - fulfilled at line 55. But total counts all requests for the location, including cancelled and on_hold ones. The in_progress count should exclude non-active statuses. The fix should either: (1) compute in_progress by filtering for only the active statuses (like new, sourcing, shortlisted, interviewing, offer_pending), or (2) subtract cancelled and on_hold counts in addition to fulfilled. The active_statuses list is already defined at line 25 and could be reused for this purpose.
Was this helpful? React with 👍 or 👎 to provide feedback.
Summary
Adds the complete Phase 1 foundation for an AI-enabled Staffing & Onboarding Portal ("FM Portal") designed to manage ~700 team members across 5 locations. This is a greenfield project temporarily hosted in this repo due to repo access constraints — it introduces an entirely new Python/React codebase (not related to the existing .NET/Angular microservices).
Backend (FastAPI + Python 3.12):
/api/v1/...).xls/.xlsxfile attachment support/frontend/dist) for single-server deploymentFrontend (React 19 + TypeScript + Material UI + Vite):
Infrastructure:
docker-compose.ymlwith PostgreSQL 16, FastAPI backend, React frontendUpdates since last revision
Rebuilt the entire frontend with a lightweight, polished design using Fannie Mae branding:
#00263e), teal (#00838f), orange (#e87722), green (#2e8540), and red (#d63e04) throughout all pages, replacing the previous generic MUI colorssize→item xs md) and type field alignment (min_experience→experience_min,unit_code→code, etc.)/frontend/dist, enabling single-port deploymentPrevious fixes (still included):
titlefield,headcount→number_of_positionsfull_name→first_name/last_name,experience_years→years_of_experienceReview & Testing Checklist for Human
RubricsPage.tsx(~80 lines). This works for demo purposes but is not how rubrics would be managed in production (they'd be uploaded via the API and stored in the DB).<Grid item xs={6} md={3}>syntax (MUI v5 style). Verify this is compatible with the installed MUI version — if MUI v6 is installed, thesizeprop syntax may be needed instead.serve_spacatch-all/{full_path:path}is added after the API router inmain.py. Verify it doesn't shadow any API routes — test that/api/v1/...endpoints still resolve correctly when the frontend dist exists.alembic.ini,docker-compose.yml, andseed_data.pycontain development-only credentials (staffing_user/staffing_pass, sample user passwords). Ensure these are parameterized before any production deployment.Recommended test plan:
cd frontend && npm install && npm run build— verify TypeScript compilation succeedsdocker-compose up --buildand verify all 3 services starthttp://localhost:8000/docs— confirm Swagger UI loads with all endpoint groupspython seed_data.pyand verify seed data viaGET /api/v1/locations/(5 locations) andGET /api/v1/practice-units/(6 units)http://localhost:8000— confirm the FM Portal dashboard renders with wave plan, hybrid work compliance, rotation tracker, locations with Company/Client labels, and practice unitsNotes
/ai/match-candidate,/ai/semantic-search, etc.) return placeholder responses — real Claude/Bedrock integration is planned for Phase 2.get_current_userreturnsNonewhen no token is provided rather than rejecting the request.SLAAttachmentupload endpoint stores a placeholder file URL (/uploads/sla/...) — actual S3 integration is deferred.Base.metadata.create_all()on startup (dev-only).passlib+bcrypt>=5.0has a known incompatibility; the seed script was verified withbcrypt==4.0.1. Pin this inrequirements.txtif you encounterAttributeError: module 'bcrypt' has no attribute '__about__'.Link to Devin session: https://partner-workshops.devinenterprise.com/sessions/ffd634558e524d0980e02b3caabdf6a3