Skip to content

feat: implement AI-powered requirement analysis with hidden internal pricing logic (#76)#81

Closed
devin-ai-integration[bot] wants to merge 4 commits into
mainfrom
devin/1781985456-ai-estimation
Closed

feat: implement AI-powered requirement analysis with hidden internal pricing logic (#76)#81
devin-ai-integration[bot] wants to merge 4 commits into
mainfrom
devin/1781985456-ai-estimation

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds an AI-powered project estimation system where clients enter a natural-language project description and receive an instant cost/timeline estimate. Supersedes #77, #79, #80 — restructured per review feedback to remove Firebase Cloud Functions (which require the paid Blaze plan) and run everything client-side.

Architecture (two-phase design, no Cloud Functions)

Client (browser)
─────────────────
  1. Read PricingConfig from Firestore (falls back to built-in defaults)
  2. Send feature category NAMES to Gemini API for classification
     (AI never sees prices, multipliers, or formulas)
  3. computeEstimate(classification, pricingConfig) — deterministic
  4. Save result to Firestore estimations collection
  5. Display: features, cost range, timeline, explanation

The AI prompt receives only feature category names for classification. All cost/timeline/explanation generation is deterministic code in computeEstimate(). The Gemini API key is provided via VITE_GEMINI_API_KEY env var.

What's new

src/dashboard/services/estimationService.ts — Client-side estimation engine:

  • getPricingConfig() reads Firestore pricingConfig/default, merges with defaults, validates all numeric fields
  • buildClassificationPrompt() sends only category names to Gemini (no prices)
  • validateClassification(data, allowedCategories) — strict type/enum/category validation
  • computeEstimate() — deterministic cost, timeline, and explanation generation
  • analyzeProject(description, userId) — orchestrates the full flow
  • fetchEstimationHistory(uid) — queries user's past estimations

src/dashboard/pages/ProjectEstimation.tsx — Client estimation page:

  • Textarea form for free-form project descriptions
  • Animated results: project type, feature breakdown table, cost range, timeline, explanation
  • Expandable estimation history

src/dashboard/pages/PricingConfig.tsx — Admin pricing config page:

  • No hardcoded pricing in the frontend bundle — loads from Firestore with access-denied handling
  • Editable grid of feature prices, complexity multipliers, estimation rules
  • Persists to pricingConfig/default in Firestore

firestore.indexes.json — Composite index for estimations query (userId ASC + createdAt DESC)

Setup

  1. Add VITE_GEMINI_API_KEY=<your-key> to .env
  2. Deploy Firestore indexes: firebase deploy --only firestore:indexes
  3. Set Firestore security rules to restrict pricingConfig to admin-only access

Closes #76

Link to Devin session: https://app.devin.ai/sessions/c46ef0652e464fe8b81fbb3cc5147eb3
Requested by: @hrx01-dev

Summary by CodeRabbit

Release Notes

  • New Features
    • AI-powered project cost and timeline estimation tool with detailed feature breakdown, complexity analysis, and cost projections
    • Pricing configuration management interface for setting feature base prices, complexity multipliers, and estimation rule parameters
    • Estimation history panel to view and review previous project estimates
    • Two new dashboard navigation items: "AI Estimate" and "Pricing Config"

hrx01-dev and others added 4 commits June 20, 2026 19:25
…ic (#76)

- Add Firebase Cloud Function (analyzeProject) that reads pricing config
  from Firestore and calls Gemini API server-side, returning only safe
  client-facing data (features, complexity, cost range, timeline, explanation)
- Add ProjectEstimation dashboard page for clients to submit project
  descriptions and receive instant AI-generated cost estimates
- Add PricingConfig admin page to manage feature prices, complexity
  multipliers, and estimation rules in Firestore
- Add estimation history with expandable records
- Store all pricing logic, prompts, and formulas exclusively server-side;
  API responses never leak internal calculation data
- Add routes (/dashboard/estimation, /dashboard/pricing-config) and
  sidebar navigation entries
- Update firebase.json with functions configuration

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…rministic pricing

- AI prompt now only performs feature extraction/classification with
  category names (no prices, multipliers, or formulas ever sent to AI)
- Cost calculation, timeline estimation, and explanation generation all
  happen deterministically server-side in computeEstimate()
- getPricingConfig() now merges Firestore data with defaults and
  validates numeric bounds to prevent partial/corrupt config
- validateClassification() performs thorough type/enum validation of
  AI output before any downstream processing
- No free-form AI text is returned to the client; explanation is built
  server-side from classification metadata

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…store index

- getPricingConfig: validate all numeric fields, feature prices, and multipliers
- validateClassification: reject categories not in allowed pricing set
- Trim description server-side before validation
- PricingConfig.tsx: remove hardcoded DEFAULT_CONFIG with pricing values;
  load purely from Firestore with access-denied handling
- Add firestore.indexes.json with composite index for estimations query
  (userId ASC + createdAt DESC)

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…nt-side

- Remove functions/ directory entirely (Cloud Functions require Blaze plan)
- Install @google/generative-ai as frontend dependency
- Move AI classification + deterministic cost computation to client-side
  estimationService with same two-phase architecture:
  Phase 1: Gemini classifies features (sees category names only, never prices)
  Phase 2: Client computes costs deterministically from pricing config
- Pricing config read from Firestore with hardened validation and defaults
- API key provided via VITE_GEMINI_API_KEY environment variable
- Remove functions config from firebase.json, revert eslint ignores

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@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, CI, and merge conflict monitoring

@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds two new dashboard features: an AI-powered project estimation page (ProjectEstimation) that calls Gemini to classify project descriptions and compute cost/timeline estimates, and a PricingConfig admin page that persists pricing rules to Firestore. Introduces new TypeScript interfaces, a Firestore composite index, and wires both pages into routing and navigation.

Changes

AI Estimation & Pricing Config Feature

Layer / File(s) Summary
Shared types, Firestore index, and Gemini dependency
src/dashboard/types.ts, firestore.indexes.json, firebase.json, package.json
Adds FeatureAnalysis, EstimationResult, and EstimationRecord interfaces; defines a Firestore composite index on estimations (userId ASC, createdAt DESC); registers the index file in firebase.json; adds @google/generative-ai dependency.
Estimation service: pricing config, Gemini prompt, and computation
src/dashboard/services/estimationService.ts
Implements getPricingConfig (Firestore read with deep-merge and validation), buildClassificationPrompt (Gemini JSON schema instruction), validateClassification (AI response validation), computeEstimate (cost/timeline with risk multiplier, buffer, and clamping), analyzeProject (end-to-end flow writing to Firestore estimations), and fetchEstimationHistory (Firestore query by userId).
PricingConfig admin page
src/dashboard/pages/PricingConfig.tsx
Reads pricingConfig/default from Firestore on mount with permission-denied detection; provides handlers for feature price CRUD, complexity multiplier edits, and estimation rule edits; renders editor sections with Save/Reset actions and success/error banners.
ProjectEstimation page: form, results, and history
src/dashboard/pages/ProjectEstimation.tsx
Implements ComplexityBadge, EstimationResults (feature breakdown table, cost/timeline cards, AI explanation), HistoryItem (expandable past estimate), and the main ProjectEstimation page managing form submission, results state, and history loading.
Routing and navigation wiring
src/app/App.tsx, src/dashboard/components/DashboardLayout.tsx
Adds estimation and pricing-config child routes under /dashboard; extends NAV_ITEMS with "AI Estimate" and "Pricing Config" sidebar entries using Sparkles and Settings2 icons.

Sequence Diagram(s)

sequenceDiagram
  actor User
  participant ProjectEstimation
  participant analyzeProject
  participant GeminiAPI
  participant Firestore

  User->>ProjectEstimation: Submit project description
  ProjectEstimation->>analyzeProject: analyzeProject(description, userId)
  analyzeProject->>Firestore: getDoc(pricingConfig/default)
  Firestore-->>analyzeProject: pricing rules (or defaults)
  analyzeProject->>GeminiAPI: generateContent(classificationPrompt + description)
  GeminiAPI-->>analyzeProject: JSON {projectType, overallComplexity, features, hasSignificantUnknowns}
  analyzeProject->>analyzeProject: computeEstimate(classification, pricing)
  analyzeProject->>Firestore: addDoc(estimations, {userId, description, result, createdAt})
  analyzeProject-->>ProjectEstimation: EstimationResult
  ProjectEstimation-->>User: Render cost/timeline/feature breakdown
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • hrx01-dev/Servio#51: Adds AuthProvider and /signin//signup routes to App.tsx; the current PR's ProjectEstimation page uses useAuth() and gates history loading on currentUser, making them directly connected at the auth/routing level.
  • hrx01-dev/Servio#68: Introduces the /dashboard nested routing shell and NAV_ITEMS in DashboardLayout.tsx that this PR extends with the two new pages and sidebar entries.

Suggested reviewers

  • hrx01-dev

Poem

🐇 A sparkle of Gemini lights the way,
Features and costs computed today.
Firestore holds the pricing so dear,
Hidden from clients — no formulas here!
The rabbit hops through each estimate's flow,
And history panels put on a show. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main objective: implementing an AI-powered requirement analysis system with hidden pricing logic, matching the PR scope and primary feature addition.
Linked Issues check ✅ Passed The PR addresses all core objectives from issue #76: AI feature analysis via Gemini (estimationService.ts), hidden pricing logic maintained client-side with Firestore config, transparent client output (ProjectEstimation.tsx), admin configuration UI (PricingConfig.tsx), and security enforcement via environment variables.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the AI-powered estimation system implementation. Package.json addition of @google/generative-ai is necessary for the Gemini API integration, firebase.json configuration is required for Firestore indexes, and all new components/services support the estimation feature.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch devin/1781985456-ai-estimation

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

Copy link
Copy Markdown

Visit the preview URL for this PR (updated for commit c7e9ea0):

https://servio-0--pr81-devin-1781985456-ai-05tfjrkc.web.app

(expires Sat, 27 Jun 2026 19:59:05 GMT)

🔥 via Firebase Hosting GitHub Action 🌎

Sign: 15915abb5951eb298a844eda460b24f444d93a69

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🧹 Nitpick comments (2)
src/dashboard/services/estimationService.ts (2)

373-378: ⚡ Quick win

Firestore write may fail silently if security rules deny access.

If Firestore rules reject the write for unauthenticated or unauthorized users, the error propagates but the user sees a generic failure. Consider validating userId is non-empty before attempting the write.

🛡️ Proposed validation
+  if (!userId) {
+    throw new Error("You must be signed in to save estimation history.");
+  }
+
   await addDoc(collection(db, "estimations"), {
     userId,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/dashboard/services/estimationService.ts` around lines 373 - 378, The
addDoc call that writes to the "estimations" collection does not validate that
userId is non-empty before attempting the write. Add a validation check before
the addDoc call to ensure userId is populated and non-empty, and throw a
descriptive error if validation fails. This prevents silent Firestore failures
when security rules reject writes due to missing or invalid userId values.

217-218: ⚡ Quick win

Hardcoded fallback multiplier 1.3 duplicates existing constant.

This magic number should reference a named constant or use the "medium" multiplier from the pricing config to stay consistent if defaults change.

♻️ Suggested fix
-    const multiplier = pricing.complexityMultipliers[f.complexity] ?? 1.3;
+    const multiplier =
+      pricing.complexityMultipliers[f.complexity] ??
+      pricing.complexityMultipliers["medium"] ??
+      1.3;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/dashboard/services/estimationService.ts` around lines 217 - 218, The
hardcoded fallback multiplier value 1.3 in the complexity multipliers lookup (in
the line with pricing.complexityMultipliers[f.complexity] ?? 1.3) is a magic
number that duplicates configuration logic. Replace this hardcoded 1.3 with
either a named constant defined at the top of the file or by referencing an
existing default multiplier from the pricing configuration object (such as the
"medium" complexity level multiplier). This ensures the fallback behavior stays
synchronized with the pricing config defaults if they change in the future.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/dashboard/pages/PricingConfig.tsx`:
- Around line 40-52: The EMPTY_CONFIG constant contains values
(minimumProjectCost: 0, maximumProjectCost: 0) that could fail validation in the
estimationService.getPricingConfig() validation logic, causing silent fallback
to defaults when admins attempt to save partial changes. Add client-side
validation checks before the PricingConfig is saved to the service; these checks
should verify that the values meet the same constraints as those checked in
estimationService (such as ensuring minimumProjectCost and maximumProjectCost
have valid relationship), and display validation error messages to the user if
the config would be rejected, preventing the silent failure scenario.
- Around line 376-383: The input field for the multiplier has min={0}, which
allows invalid values that would silently fall back to defaults since
estimationService.getPricingConfig() requires multiplier > 0. Change the min
attribute from 0 to 0.1 in the number input element that calls updateMultiplier
in its onChange handler to enforce the valid range at the UI level.
- Around line 469-480: The riskFactorMultiplier input field in PricingConfig.tsx
has a minimum value of 0, but the backend validation in estimationService.ts
requires the value to be greater than 0 (values <= 0 trigger fallback to
defaults). Change the min attribute on the number input from 0 to a value
greater than 0 (such as 0.05 to match the existing step value) to prevent users
from entering invalid values that will be rejected by the backend.
- Around line 61-105: Add a proactive admin role check in the PricingConfig
component to verify the authenticated user has admin privileges before rendering
the form and attempting data fetch. Retrieve the current authenticated user and
check their admin status/claim using a utility function, then use this check to
conditionally set the accessDenied state early (before or alongside the
loadConfig useEffect). This prevents any authenticated non-admin user from
seeing the pricing config form UI and making Firestore requests, rather than
relying solely on catching permission errors after the fact in the catch block
of loadConfig.

In `@src/dashboard/pages/ProjectEstimation.tsx`:
- Around line 294-297: The analyzeProject function call on line 296 uses an
empty string fallback for userId when currentUser is absent, which creates
orphaned records. Before calling analyzeProject in the estimation submission
flow, add a validation check to ensure currentUser?.uid exists and is non-empty,
and prevent form submission if the user ID is not available. Apply the same
validation fix to the similar code referenced on line 403.
- Around line 343-351: The History button with the handleLoadHistory onClick
handler is hidden on small screens due to the `hidden sm:flex` Tailwind classes
in the className prop, making it inaccessible to mobile users. Remove the
`hidden sm:flex` classes from the Button component so that the History button is
visible and accessible on all viewport sizes, including mobile.

In `@src/dashboard/services/estimationService.ts`:
- Around line 359-365: The code in the JSON parsing logic in
estimationService.ts does not handle empty responses from the Gemini model
before attempting to parse. After calling result.response.text() to get the
responseText, add a guard check to validate that responseText is not empty or
whitespace before attempting JSON.parse(). If the response is empty, throw a
descriptive error immediately indicating the AI returned an empty response,
which helps distinguish this case from actual JSON parsing errors in the catch
block.
- Around line 327-332: The Gemini API key is currently being exposed in the
client-side bundle by being used directly with the GoogleGenerativeAI client
library in the estimationService. Instead of making direct calls to the Gemini
API from the browser, refactor the code to call a backend endpoint that you will
create. Remove the VITE_GEMINI_API_KEY retrieval and GoogleGenerativeAI client
instantiation from estimationService, and replace those calls with HTTP requests
to a new backend proxy endpoint. The backend endpoint should securely store the
VITE_GEMINI_API_KEY on the server side and handle all communication with the
Gemini API, returning only the necessary results to the frontend. This ensures
the API key never reaches the browser.

---

Nitpick comments:
In `@src/dashboard/services/estimationService.ts`:
- Around line 373-378: The addDoc call that writes to the "estimations"
collection does not validate that userId is non-empty before attempting the
write. Add a validation check before the addDoc call to ensure userId is
populated and non-empty, and throw a descriptive error if validation fails. This
prevents silent Firestore failures when security rules reject writes due to
missing or invalid userId values.
- Around line 217-218: The hardcoded fallback multiplier value 1.3 in the
complexity multipliers lookup (in the line with
pricing.complexityMultipliers[f.complexity] ?? 1.3) is a magic number that
duplicates configuration logic. Replace this hardcoded 1.3 with either a named
constant defined at the top of the file or by referencing an existing default
multiplier from the pricing configuration object (such as the "medium"
complexity level multiplier). This ensures the fallback behavior stays
synchronized with the pricing config defaults if they change in the future.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 306d5e0d-d61c-42c0-adaa-a345c94db083

📥 Commits

Reviewing files that changed from the base of the PR and between 92f57e1 and c7e9ea0.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (9)
  • firebase.json
  • firestore.indexes.json
  • package.json
  • src/app/App.tsx
  • src/dashboard/components/DashboardLayout.tsx
  • src/dashboard/pages/PricingConfig.tsx
  • src/dashboard/pages/ProjectEstimation.tsx
  • src/dashboard/services/estimationService.ts
  • src/dashboard/types.ts

Comment on lines +40 to +52
const EMPTY_CONFIG: PricingConfig = {
featurePricing: {},
complexityMultipliers: {
low: 1.0,
medium: 1.3,
high: 1.7,
enterprise: 2.2,
},
minimumProjectCost: 0,
maximumProjectCost: 0,
bufferPercentage: 0,
riskFactorMultiplier: 1.0,
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

EMPTY_CONFIG values will fail validation in estimationService.getPricingConfig().

minimumProjectCost: 0, maximumProjectCost: 0, and riskFactorMultiplier: 1.0 (though valid) create a state where maximumProjectCost < minimumProjectCost is false only because both are 0. However, if an admin saves partial changes, the validation at lines 107-120 in estimationService.ts may reject the config and silently fall back to defaults, making admin edits appear to have no effect.

Consider adding client-side validation warnings before save.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/dashboard/pages/PricingConfig.tsx` around lines 40 - 52, The EMPTY_CONFIG
constant contains values (minimumProjectCost: 0, maximumProjectCost: 0) that
could fail validation in the estimationService.getPricingConfig() validation
logic, causing silent fallback to defaults when admins attempt to save partial
changes. Add client-side validation checks before the PricingConfig is saved to
the service; these checks should verify that the values meet the same
constraints as those checked in estimationService (such as ensuring
minimumProjectCost and maximumProjectCost have valid relationship), and display
validation error messages to the user if the config would be rejected,
preventing the silent failure scenario.

Comment on lines +61 to +105
export function PricingConfig() {
const [config, setConfig] = useState<PricingConfig | null>(null);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState(false);
const [accessDenied, setAccessDenied] = useState(false);
const [newFeatureKey, setNewFeatureKey] = useState("");
const [newFeaturePrice, setNewFeaturePrice] = useState("");

useEffect(() => {
async function loadConfig() {
try {
const snap = await getDoc(doc(db, "pricingConfig", "default"));
if (snap.exists()) {
const raw = snap.data() as Partial<PricingConfig>;
setConfig({
featurePricing: raw.featurePricing ?? {},
complexityMultipliers: raw.complexityMultipliers ?? {
low: 1.0,
medium: 1.3,
high: 1.7,
enterprise: 2.2,
},
minimumProjectCost: raw.minimumProjectCost ?? 0,
maximumProjectCost: raw.maximumProjectCost ?? 0,
bufferPercentage: raw.bufferPercentage ?? 0,
riskFactorMultiplier: raw.riskFactorMultiplier ?? 1.0,
});
} else {
setConfig(EMPTY_CONFIG);
}
} catch (err) {
const message = err instanceof Error ? err.message : "";
if (message.includes("permission") || message.includes("PERMISSION_DENIED")) {
setAccessDenied(true);
} else {
setConfig(EMPTY_CONFIG);
}
} finally {
setLoading(false);
}
}
loadConfig();
}, []);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for Firestore security rules file
fd -e rules -i firestore
cat firestore.rules 2>/dev/null || echo "No firestore.rules found in repo root"

Repository: hrx01-dev/Servio

Length of output: 98


🏁 Script executed:

# Search for firestore rules files throughout the entire repo
fd -type f -name '*.rules' -o -name '*firestore*' -o -name '*security*' | grep -i rule

Repository: hrx01-dev/Servio

Length of output: 231


🏁 Script executed:

# Check the directory structure to understand the project layout
git ls-files | head -30

Repository: hrx01-dev/Servio

Length of output: 743


🏁 Script executed:

# Look for authentication/authorization related files and utilities
fd -type f \( -name '*auth*' -o -name '*permission*' -o -name '*role*' \) | grep -E '\.(ts|tsx|js|json)$'

Repository: hrx01-dev/Servio

Length of output: 231


🏁 Script executed:

# Check routing configuration to see if there are route guards
fd -type f -name '*route*' -o -name '*router*' | grep -E '\.(ts|tsx|js)$'

Repository: hrx01-dev/Servio

Length of output: 231


🏁 Script executed:

# Examine the PricingConfig component more thoroughly
cat src/dashboard/pages/PricingConfig.tsx

Repository: hrx01-dev/Servio

Length of output: 16563


🏁 Script executed:

# Search for files with .rules extension
find . -name "*.rules" -type f 2>/dev/null

Repository: hrx01-dev/Servio

Length of output: 42


🏁 Script executed:

# Check firebase.json for rules configuration
cat firebase.json 2>/dev/null | head -50

Repository: hrx01-dev/Servio

Length of output: 781


🏁 Script executed:

# Search for route protection/guards and admin checks
rg "admin|guard|protect|role|permission|claim" --type ts --type tsx -i | head -40

Repository: hrx01-dev/Servio

Length of output: 88


🏁 Script executed:

# Look for the route definition of pricing-config
rg "pricing-config" --type ts --type tsx

Repository: hrx01-dev/Servio

Length of output: 88


🏁 Script executed:

# Check if there's any authentication wrapper or context usage in relevant components
rg "PricingConfig|pricing" -A 3 -B 3 --type ts --type tsx | head -50

Repository: hrx01-dev/Servio

Length of output: 88


🏁 Script executed:

# Search for pricing-config route and admin patterns without type filter
rg "pricing-config|admin|role|permission" -A 2 -B 2 | head -60

Repository: hrx01-dev/Servio

Length of output: 2962


🏁 Script executed:

# Check how PricingConfig is imported/used in the app routing
rg "PricingConfig" -A 3 -B 3

Repository: hrx01-dev/Servio

Length of output: 5987


🏁 Script executed:

# Look for useAuth or auth context usage in components to understand auth pattern
rg "useAuth|AuthContext" --max-count 20

Repository: hrx01-dev/Servio

Length of output: 1681


🏁 Script executed:

# Check the App.tsx or routing setup to see if there's a guard on the pricing route
cat src/app/App.tsx | head -150

Repository: hrx01-dev/Servio

Length of output: 6163


🏁 Script executed:

# Check ProtectedRoute to see what it actually protects
cat src/dashboard/components/ProtectedRoute.tsx

Repository: hrx01-dev/Servio

Length of output: 858


🏁 Script executed:

# Check useAuth hook to see what claims/data it provides
cat src/Firebase/useAuth.ts

Repository: hrx01-dev/Servio

Length of output: 212


🏁 Script executed:

# Check AuthContextObject for the user type definition
cat src/Firebase/AuthContextObject.ts

Repository: hrx01-dev/Servio

Length of output: 324


🏁 Script executed:

# Look for any admin claim checks or custom claims in the codebase
rg "admin|claim|custom" | grep -i user

Repository: hrx01-dev/Servio

Length of output: 42


Add client-side admin role check before rendering pricing config UI.

The /dashboard/pricing-config route is protected by ProtectedRoute, which requires authentication but does NOT verify admin privileges. Any authenticated user can render the component and its form, even though PricingConfig.tsx catches Firestore permission errors after the fact. While Firestore rules likely restrict actual read/write access (permission error handling is present), the component lacks proactive authorization checks.

Consider:

  • Adding a role/claim utility function to check for admin status from the authenticated user
  • Gating the component render with an admin check before attempting data fetch, rather than relying solely on Firestore permission errors
  • Note: Firestore security rules file not found in repository; if properly configured server-side, actual data access is protected
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/dashboard/pages/PricingConfig.tsx` around lines 61 - 105, Add a proactive
admin role check in the PricingConfig component to verify the authenticated user
has admin privileges before rendering the form and attempting data fetch.
Retrieve the current authenticated user and check their admin status/claim using
a utility function, then use this check to conditionally set the accessDenied
state early (before or alongside the loadConfig useEffect). This prevents any
authenticated non-admin user from seeing the pricing config form UI and making
Firestore requests, rather than relying solely on catching permission errors
after the fact in the catch block of loadConfig.

Comment on lines +376 to +383
<input
type="number"
min={0}
step={0.1}
value={value}
onChange={(e) =>
updateMultiplier(key, parseFloat(e.target.value) || 0)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Multiplier min={0} allows invalid values that break estimation.

estimationService.getPricingConfig() requires multiplier > 0 (line 105). Setting a multiplier to 0 here will cause silent fallback to defaults. Use min={0.1} or add validation feedback.

🛡️ Proposed fix
                     <input
                       type="number"
-                      min={0}
+                      min={0.1}
                       step={0.1}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/dashboard/pages/PricingConfig.tsx` around lines 376 - 383, The input
field for the multiplier has min={0}, which allows invalid values that would
silently fall back to defaults since estimationService.getPricingConfig()
requires multiplier > 0. Change the min attribute from 0 to 0.1 in the number
input element that calls updateMultiplier in its onChange handler to enforce the
valid range at the UI level.

Comment on lines +469 to +480
<input
type="number"
min={0}
step={0.05}
value={config.riskFactorMultiplier}
onChange={(e) =>
setConfig((prev) => {
if (!prev) return prev;
return {
...prev,
riskFactorMultiplier: parseFloat(e.target.value) || 0,
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Risk factor multiplier allows 0, which fails backend validation.

Same issue as complexity multipliers—riskFactorMultiplier <= 0 triggers fallback to defaults per estimationService.ts line 115.

🛡️ Proposed fix
                 <input
                   type="number"
-                  min={0}
+                  min={0.01}
                   step={0.05}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<input
type="number"
min={0}
step={0.05}
value={config.riskFactorMultiplier}
onChange={(e) =>
setConfig((prev) => {
if (!prev) return prev;
return {
...prev,
riskFactorMultiplier: parseFloat(e.target.value) || 0,
};
<input
type="number"
min={0.01}
step={0.05}
value={config.riskFactorMultiplier}
onChange={(e) =>
setConfig((prev) => {
if (!prev) return prev;
return {
...prev,
riskFactorMultiplier: parseFloat(e.target.value) || 0,
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/dashboard/pages/PricingConfig.tsx` around lines 469 - 480, The
riskFactorMultiplier input field in PricingConfig.tsx has a minimum value of 0,
but the backend validation in estimationService.ts requires the value to be
greater than 0 (values <= 0 trigger fallback to defaults). Change the min
attribute on the number input from 0 to a value greater than 0 (such as 0.05 to
match the existing step value) to prevent users from entering invalid values
that will be rejected by the backend.

Comment on lines +294 to +297
const estimation = await analyzeProject(
description.trim(),
currentUser?.uid ?? "",
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid submitting with an empty userId fallback.

Line 296 falls back to "" when currentUser is absent. That can cause failed writes or inconsistent/orphaned estimation records. Block submit until currentUser.uid exists.

Proposed fix
   const handleSubmit = async (e: React.FormEvent) => {
     e.preventDefault();
+    if (!currentUser?.uid) {
+      setError("You must be signed in to create an estimate.");
+      return;
+    }
     if (!description.trim() || description.trim().length < 10) {
       setError("Please provide a detailed project description (at least 10 characters).");
       return;
     }
@@
       const estimation = await analyzeProject(
         description.trim(),
-        currentUser?.uid ?? "",
+        currentUser.uid,
       );
@@
-                  disabled={loading || !description.trim()}
+                  disabled={loading || !description.trim() || !currentUser?.uid}

Also applies to: 403-403

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/dashboard/pages/ProjectEstimation.tsx` around lines 294 - 297, The
analyzeProject function call on line 296 uses an empty string fallback for
userId when currentUser is absent, which creates orphaned records. Before
calling analyzeProject in the estimation submission flow, add a validation check
to ensure currentUser?.uid exists and is non-empty, and prevent form submission
if the user ID is not available. Apply the same validation fix to the similar
code referenced on line 403.

Comment on lines +343 to +351
<Button
variant="outline"
size="sm"
onClick={handleLoadHistory}
className="hidden sm:flex items-center gap-2"
>
<History className="h-4 w-4" />
History
</Button>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

History is inaccessible on mobile viewports.

Line 347 hides the only “History” trigger on small screens (hidden sm:flex), so users cannot open previous estimates on mobile.

Proposed fix
-            <Button
+            <Button
               variant="outline"
               size="sm"
               onClick={handleLoadHistory}
-              className="hidden sm:flex items-center gap-2"
+              className="flex items-center gap-2"
             >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Button
variant="outline"
size="sm"
onClick={handleLoadHistory}
className="hidden sm:flex items-center gap-2"
>
<History className="h-4 w-4" />
History
</Button>
<Button
variant="outline"
size="sm"
onClick={handleLoadHistory}
className="flex items-center gap-2"
>
<History className="h-4 w-4" />
History
</Button>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/dashboard/pages/ProjectEstimation.tsx` around lines 343 - 351, The
History button with the handleLoadHistory onClick handler is hidden on small
screens due to the `hidden sm:flex` Tailwind classes in the className prop,
making it inaccessible to mobile users. Remove the `hidden sm:flex` classes from
the Button component so that the History button is visible and accessible on all
viewport sizes, including mobile.

Comment on lines +327 to +332
const apiKey = import.meta.env.VITE_GEMINI_API_KEY as string | undefined;
if (!apiKey) {
throw new Error(
"AI service is not configured. Set VITE_GEMINI_API_KEY in your environment.",
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

How to restrict Google AI API key by HTTP referrer

💡 Result:

To restrict a Google AI (Gemini) API key by HTTP referrer, you must use the Google Cloud Console or the gcloud CLI to apply browser-based application restrictions [1][2][3]. Note that you can typically only apply one type of application restriction (e.g., HTTP referrers, IP addresses, Android apps, or iOS apps) to a single API key [4][5][6]. Using Google Cloud Console: 1. Navigate to the Google Cloud Console Credentials page [1][3]. 2. Select the project and click on the name of the API key you wish to restrict [1][3]. 3. Under the Application restrictions section, select HTTP referrers (websites) [2][4]. 4. Add the allowed URLs (e.g., example.com/* or .example.com/) to the allowed referrers list [2]. 5. Click Save to apply the restrictions [1][3]. Using gcloud CLI: You can update an existing key with the --allowed-referrers flag [7]: gcloud services api-keys update projects/PROJECT_ID/locations/global/keys/KEY_ID --allowed-referrers="https://www.example.com/*,http://*.example.com/" When creating a new key, you can similarly specify the flag [8][9]: gcloud services api-keys create --allowed-referrers="https://www.example.com/" Important Considerations: - API Permissions: You must have the apikeys.keys.update permission on the Google Cloud project to modify these settings [1][3]. - Security: Restricting your key is a security best practice that limits the potential damage if the key is exposed [4][3]. - Separate Keys: If you need to use the key for different environments or client types (e.g., both a web app and a backend server), create separate, restricted API keys for each [4].

Citations:


🏁 Script executed:

# First, let's look at the file structure and the specific lines mentioned
head -350 src/dashboard/services/estimationService.ts | tail -50

Repository: hrx01-dev/Servio

Length of output: 1524


🏁 Script executed:

# Search for how the apiKey is used after it's retrieved
rg "apiKey" src/dashboard/services/estimationService.ts -A 5 -B 2

Repository: hrx01-dev/Servio

Length of output: 549


🏁 Script executed:

# Check if there are any server-side Gemini API implementations
fd -e ts -e js | xargs rg "GEMINI" 2>/dev/null | grep -v node_modules

Repository: hrx01-dev/Servio

Length of output: 308


🏁 Script executed:

# Check if there's any server-side implementation or backend API
fd -e ts -e js | xargs rg "generateContent|GoogleGenerativeAI" 2>/dev/null | head -20

Repository: hrx01-dev/Servio

Length of output: 347


🏁 Script executed:

# Look for environment configuration and build setup
cat vite.config.ts 2>/dev/null || cat vite.config.js 2>/dev/null || echo "No vite config found in root"

Repository: hrx01-dev/Servio

Length of output: 992


🌐 Web query:

Gemini API client-side usage security best practices Google recommendations

💡 Result:

Google provides strict recommendations for securing Gemini API usage, emphasizing that API keys should never be exposed in client-side code [1][2][3]. Hardcoding or embedding keys in web or mobile applications makes them extractable by users, leading to potential unauthorized access, quota exhaustion, and unexpected billing charges [1][4][5]. Core Best Practices 1. Use a Backend Proxy: For production applications, implement a backend proxy server to handle API calls [1][2]. The frontend should communicate with your own backend, which then securely adds the API credentials and interacts with the Gemini API [4][5]. This ensures your API keys remain on the server and are never exposed to the client [1][6][7]. 2. Implement Firebase App Check: If you are building mobile or web apps that require client-side interaction, Google strongly recommends using Vertex AI in Firebase SDKs combined with Firebase App Check [6][8]. App Check verifies that incoming API requests are originating from your legitimate, authorized application, effectively preventing abuse by unauthorized clients [9][10]. 3. Enforce API and Application Restrictions: Always apply strict restrictions to your API keys in the Google Cloud Console [11][4]. - API Restrictions: Limit the key to only the specific services it requires (e.g., Gemini API) [11]. - Application Restrictions: Restrict the key to specific websites or IP addresses to prevent it from being used elsewhere [11]. 4. Proper Secret Management: - Never commit API keys to version control systems like Git [1][2]. - Use secure storage services like Google Cloud Secret Manager to manage your keys [1][11]. - Utilize environment variables in your server-side environment rather than configuration files [1][2]. 5. Monitor and Protect Usage: - Set up billing alerts in the Google Cloud Console to get notified of usage spikes [1][2]. - Periodically rotate your API keys by creating new ones and deleting old, unused credentials [4]. If you are currently exposing keys in your client-side code, Google recommends deleting the compromised keys immediately via Google AI Studio or the Google Cloud Console, issuing new keys, and migrating to a backend proxy or a Firebase-based architecture [6][8][5].

Citations:


Move Gemini API calls to a backend proxy to secure the API key.

The VITE_GEMINI_API_KEY is exposed in the client-side bundle because it's instantiated with the GoogleGenerativeAI client library in the browser. Google's official guidance explicitly recommends using a backend proxy as the primary solution—have the frontend call your own backend, which securely adds credentials and communicates with Gemini API. HTTP referrer restrictions and usage quotas are helpful secondary measures but do not eliminate the core risk of key extraction. A backend proxy prevents the key from ever reaching the browser.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/dashboard/services/estimationService.ts` around lines 327 - 332, The
Gemini API key is currently being exposed in the client-side bundle by being
used directly with the GoogleGenerativeAI client library in the
estimationService. Instead of making direct calls to the Gemini API from the
browser, refactor the code to call a backend endpoint that you will create.
Remove the VITE_GEMINI_API_KEY retrieval and GoogleGenerativeAI client
instantiation from estimationService, and replace those calls with HTTP requests
to a new backend proxy endpoint. The backend endpoint should securely store the
VITE_GEMINI_API_KEY on the server side and handle all communication with the
Gemini API, returning only the necessary results to the frontend. This ensures
the API key never reaches the browser.

Comment on lines +359 to +365
const responseText = result.response.text();
let parsed: unknown;
try {
parsed = JSON.parse(responseText);
} catch {
throw new Error("Failed to parse AI response. Please try again.");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Empty or malformed AI response can cause unhandled exception.

If the Gemini model returns an empty response (e.g., content filtered, rate limited), result.response.text() may return an empty string causing JSON.parse("") to throw a SyntaxError with a less informative message than the current catch provides.

🛡️ Proposed guard for empty response
   const responseText = result.response.text();
+  if (!responseText || responseText.trim().length === 0) {
+    throw new Error("AI returned an empty response. Please try again.");
+  }
   let parsed: unknown;
   try {
     parsed = JSON.parse(responseText);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/dashboard/services/estimationService.ts` around lines 359 - 365, The code
in the JSON parsing logic in estimationService.ts does not handle empty
responses from the Gemini model before attempting to parse. After calling
result.response.text() to get the responseText, add a guard check to validate
that responseText is not empty or whitespace before attempting JSON.parse(). If
the response is empty, throw a descriptive error immediately indicating the AI
returned an empty response, which helps distinguish this case from actual JSON
parsing errors in the catch block.

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

Superseded — final fixes applied.

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.

Implement AI-Powered Requirement Analysis with Hidden Internal Pricing Logic

1 participant