Skip to content

Phase 1 Foundation: FM Portal — AI-Enabled Staffing & Onboarding#225

Open
devin-ai-integration[bot] wants to merge 7 commits into
mainfrom
devin/1775857858-phase1-v2
Open

Phase 1 Foundation: FM Portal — AI-Enabled Staffing & Onboarding#225
devin-ai-integration[bot] wants to merge 7 commits into
mainfrom
devin/1775857858-phase1-v2

Conversation

@devin-ai-integration
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot commented Apr 10, 2026

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):

  • 15+ SQLAlchemy models: User, Location, Role, Candidate, StaffingRequest, InterviewRound/Feedback, Offer, OnboardingTask, HybridWorkAgreement, RotationTracker, SLADefinition/Tracking/Attachment, PracticeUnit, UnitAnchor, RoleRubric/Criterion/Rating, AuditLog, Notification
  • Full CRUD API endpoints across 11 route groups (/api/v1/...)
  • AI service abstraction layer with Claude (Anthropic direct + Amazon Bedrock) providers — currently returns placeholder/mock data
  • Role rubric management with HTML file upload and real-time rendering endpoint
  • SLA management with .xls/.xlsx file attachment support
  • Practice Unit + Unit Anchor lifecycle tracking
  • Auth service with Infosys SSO placeholder, JWT token generation
  • WMT (Workplace Management Tool) integration placeholder
  • Alembic migration config, seed data script, Docker setup
  • Static frontend serving from FastAPI (/frontend/dist) for single-server deployment

Frontend (React 19 + TypeScript + Material UI + Vite):

  • 8 pages: Dashboard, Staffing Requests, Candidates, Roles, Practice Units, Rubrics, SLA Management, Semantic Search
  • FM Portal branding with Fannie Mae color scheme and Source Sans Pro typography
  • Wave-Based Staffing Plan dashboard (4 waves: 70 + 180 + 250 + 200 = 700 over 12 months)
  • Hybrid Work Compliance section (3 days/week in-office requirement tracking)
  • 24-Month Rotation Tracker with due date chips (30/90/180-day alerts)
  • OCP Hosting rubric sample embedded with sandboxed iframe preview
  • Admin button in top bar (placeholder for future configuration features)
  • Typed API service layer using Axios

Infrastructure:

  • docker-compose.yml with PostgreSQL 16, FastAPI backend, React frontend
  • Dockerfiles for both services

Updates since last revision

Rebuilt the entire frontend with a lightweight, polished design using Fannie Mae branding:

  1. Renamed to "FM Portal" — Updated title, sidebar logo, and all references from "Staffing AI Portal" to "FM Portal"
  2. Fannie Mae color scheme — Applied dark navy (#00263e), teal (#00838f), orange (#e87722), green (#2e8540), and red (#d63e04) throughout all pages, replacing the previous generic MUI colors
  3. Source Sans Pro font — Added Google Fonts link for Source Sans 3 to match Fannie Mae typography
  4. Wave-Based Staffing Plan — Added prominent dashboard card showing 4 hiring waves over 12 months with progress bars and position counts per wave
  5. Hybrid Work Compliance — Added dashboard section highlighting the 3 days/week in-office requirement with compliant/pending/non-compliant counts
  6. 24-Month Rotation Tracker — Added dashboard section with rotation status chips (active, due 30/90/180 days, overdue)
  7. OCP Hosting rubric embedded — RubricsPage now includes the user-provided Virtual & OCP Hosting interview rubric HTML as pre-loaded demo data with a preview dialog
  8. Admin button — Added to the AppBar for future configuration features
  9. Location type labels — Dashboard locations table now shows "Company" or "Client" labels with color-coded chips
  10. TypeScript fixes — Fixed Grid props (sizeitem xs md) and type field alignment (min_experienceexperience_min, unit_codecode, etc.)
  11. Static serving — FastAPI now serves the built frontend from /frontend/dist, enabling single-port deployment

Previous fixes (still included):

  • StaffingRequestsPage missing title field, headcountnumber_of_positions
  • CandidatesPage full_namefirst_name/last_name, experience_yearsyears_of_experience
  • RubricsPage XSS fix (sandboxed iframe + CSP headers)
  • seed_data.py Location/User field fixes

Review & Testing Checklist for Human

  • Confirm target repo: This codebase is an entirely new project pushed here due to access constraints. It should likely be moved to its own dedicated repository before merging.
  • Verify hardcoded demo data: Several sections use hardcoded/static data rather than API responses — the wave plan (4 waves), SLA definitions (6 demo SLAs), and rubric list (3 demo entries) are frontend-only constants. Verify this is acceptable for the demo, or if these should be backed by API endpoints.
  • OCP rubric as inline string: The rubric HTML is embedded as a large string literal in 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 prop syntax: Grid components use <Grid item xs={6} md={3}> syntax (MUI v5 style). Verify this is compatible with the installed MUI version — if MUI v6 is installed, the size prop syntax may be needed instead.
  • SPA catch-all route order: The serve_spa catch-all /{full_path:path} is added after the API router in main.py. Verify it doesn't shadow any API routes — test that /api/v1/... endpoints still resolve correctly when the frontend dist exists.
  • Hardcoded credentials: alembic.ini, docker-compose.yml, and seed_data.py contain development-only credentials (staffing_user/staffing_pass, sample user passwords). Ensure these are parameterized before any production deployment.

Recommended test plan:

  1. cd frontend && npm install && npm run build — verify TypeScript compilation succeeds
  2. docker-compose up --build and verify all 3 services start
  3. Hit http://localhost:8000/docs — confirm Swagger UI loads with all endpoint groups
  4. Run python seed_data.py and verify seed data via GET /api/v1/locations/ (5 locations) and GET /api/v1/practice-units/ (6 units)
  5. Open http://localhost:8000 — confirm the FM Portal dashboard renders with wave plan, hybrid work compliance, rotation tracker, locations with Company/Client labels, and practice units
  6. Click Admin button — confirm it appears and is clickable (placeholder for now)
  7. Navigate to Rubrics page — click the preview (eye) icon on "Virtual & OCP Hosting Rubric" — verify the embedded rubric HTML renders in the preview dialog
  8. Navigate to each page (Requests, Candidates, Roles, Practice Units, SLA, Search) and verify Fannie Mae styling is consistent

Notes

  • AI endpoints (/ai/match-candidate, /ai/semantic-search, etc.) return placeholder responses — real Claude/Bedrock integration is planned for Phase 2.
  • Auth is permissive in dev mode — get_current_user returns None when no token is provided rather than rejecting the request.
  • The SLAAttachment upload endpoint stores a placeholder file URL (/uploads/sla/...) — actual S3 integration is deferred.
  • No Alembic migration versions are included yet; tables are auto-created via Base.metadata.create_all() on startup (dev-only).
  • passlib + bcrypt>=5.0 has a known incompatibility; the seed script was verified with bcrypt==4.0.1. Pin this in requirements.txt if you encounter AttributeError: module 'bcrypt' has no attribute '__about__'.
  • The wave plan, hybrid work compliance counts, and SLA demo data are currently hardcoded in the frontend for demo purposes. These would be driven by real backend data in Phase 2.

Link to Devin session: https://partner-workshops.devinenterprise.com/sessions/ffd634558e524d0980e02b3caabdf6a3


Open with Devin

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-integration
Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

devin-ai-integration[bot]

This comment was marked as resolved.

…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)
@devin-ai-integration devin-ai-integration Bot changed the title Phase 1 Foundation: AI-Enabled Staffing & Onboarding Portal Phase 1 Foundation: FM Portal — AI-Enabled Staffing & Onboarding Apr 10, 2026
Copy link
Copy Markdown
Contributor Author

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 new potential issues.

View 7 additional findings in Devin Review.

Open in Devin Review

Comment thread backend/app/main.py
Comment on lines +61 to +63
file_path = FRONTEND_DIST / full_path
if file_path.exists() and file_path.is_file():
return FileResponse(str(file_path))
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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 path

The code should verify file_path.resolve().is_relative_to(FRONTEND_DIST.resolve()) before serving.

Suggested change
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))
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +76 to +77
interview_round: Mapped["InterviewRound"] = relationship(back_populates="feedbacks")
interviewer: Mapped["User"] = relationship()
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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.

Suggested change
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")
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 new potential issues.

View 10 additional findings in Devin Review.

Open in Devin Review

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")
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Suggested change
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")
Open in Devin Review

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,
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants