# 🧪 Fenesta GBP Dashboard — Screen-by-Screen Testing Guide

> **Generated**: April 30, 2026 | **Last Updated**: May 4, 2026
> **Project Status**: Phases 0–5 Complete ✅ | Phase 6 (Google API) & Phase 7 (Polish) Queued
> **Security Audit Applied**: May 4, 2026 — 10 bugs fixed, XSS hardening, password validation

---

## 🚀 How to Run the Project

### Prerequisites

| Requirement | Version | Check Command |
|-------------|---------|---------------|
| Python | 3.10+ | `python --version` |
| PostgreSQL | 14+ | Must be running (pgAdmin or services) |
| Node.js (optional, for live-server) | Any | `node --version` |

### Step 1: Create Virtual Environment (first time only)

```powershell
# Navigate to the GMB root (venv lives HERE, not inside backend/)
cd "c:\Users\ABCom\OneDrive - Hashtag Orange Advertising Private Limited\Desktop\GMB"
python -m venv venv
.\venv\Scripts\activate
pip install -r backend\requirements.txt
```

### Step 2: Database Setup (One-Time)

```powershell
# From GMB root with venv activated:

# Option A: If psql is on your PATH
psql -U postgres -c "CREATE DATABASE fenesta_db;"
psql -U postgres -d fenesta_db -f backend\schema.sql

# Option B: If psql is NOT on your PATH (use Python instead)
# Create the database via pgAdmin, then:
python -c "import sys; sys.path.append('backend'); from app.database import init_db; init_db(); print('All 5 tables created')"
```

### Step 3: Seed the Database

```powershell
# From GMB root with venv activated:
cd backend
python -m seed_admin
python -m seed_test_data
```

**⚠️ Full reset (drops all data):**

```powershell
# From GMB root with venv activated:
python -c "import sys; sys.path.append('backend'); from app.database import get_db_connection; conn=get_db_connection(); c=conn.cursor(); c.execute('DROP TABLE IF EXISTS trend_cache, metrics_cache, dealers, clients, users CASCADE'); conn.commit(); c.close(); conn.close(); print('Tables dropped')"
python -c "import sys; sys.path.append('backend'); from app.database import init_db; init_db(); print('Tables recreated')"
cd backend
python -m seed_admin
python -m seed_test_data
```

### Step 4: Start the Backend Server

```powershell
# From backend/ with venv activated
cd backend
python -m uvicorn app.main:app --reload
```

✅ Server runs at: **http://127.0.0.1:8000**
📖 Swagger API docs at: **http://127.0.0.1:8000/docs**

### Step 5: Serve the Frontend

Open a **new terminal** and serve the frontend:

```powershell
# Option A: Python simple server
cd frontend
python -m http.server 5500

# Option B: VS Code Live Server (right-click index.html → Open with Live Server)

# Option C: npx
cd frontend
npx -y serve .
```

✅ Frontend runs at: **http://127.0.0.1:5500** (or whichever port your server uses)

### Step 6: Open & Test

1. Open the frontend URL in your browser
2. Login with any of the test credentials below
3. Follow the screen-by-screen testing guide

> **Tip**: Keep the backend terminal visible — you'll see logs for mock post creation, sync jobs, and any errors.

---

## 📊 Project Status Summary

| Area | Status |
|------|--------|
| Auth (Login + Password Change + Strength Validation) | ✅ Working |
| Agency Dashboard (Clients + My Location) | ✅ Working |
| Client Dashboard (4 tabs: Global, Network, Add Dealer, HQ) | ✅ Working |
| Dealer Dashboard | ✅ Working |
| Cascade Deactivation (Client → Dealers) | ✅ Working |
| XSS Protection (Frontend sanitize()) | ✅ Working |
| Google OAuth | 🟡 Stub only (needs GCP credentials) |
| Real Google API | ⬜ Not started (blocked on GCP approval) |
| Background Scheduler (APScheduler) | ✅ Working |
| Trend Cache (6-month historical) | ✅ Working |

---

## Screen 1: Login Page

### What's On Screen
- Dark gradient background with glassmorphism card
- "Hashtag Orange — GBP Dealer Dashboard" title
- Email input field
- Password input field (with eye icon to show/hide)
- "Secure Login" button
- "Sign in with Google" button (with Google SVG icon)
- Red error message area (hidden by default)

### Functions & How to Test

| # | Action | How to Test | Expected Result | Background Process |
|---|--------|------------|-----------------|-------------------|
| 1 | **Email/Password Login** | Enter `admin@hashtagorang.in` / `Admin@123`, click "Secure Login" | Redirects to Agency Dashboard. Token stored in localStorage. | `POST /api/auth/login` -> bcrypt verifies password -> JWT created -> returns token |
| 2 | **Wrong password** | Enter correct email but wrong password | Red error: "Invalid password" | Backend returns 401 |
| 3 | **Unregistered email** | Enter an email that doesn't exist | Red error: "Account not registered" | Backend returns 401 |
| 4 | **Deactivated account** | Login as a deactivated user | Red error: "Account has been deactivated. Contact your administrator." | Backend returns 403 |
| 5 | **Dealer of deactivated client** | Login as a dealer whose parent client is deactivated | Red error: "Your parent account has been deactivated. Contact your manager." | Backend checks `clients.is_active` via JOIN |
| 6 | **First-time login (forced change)** | Login as `dealer.jaipur@fenesta.com` / `Dealer@123` | Password Change screen appears | Backend returns `needs_password_change: true` |
| 7 | **Google Login** | Click "Sign in with Google" | Alert: "Google login not configured." | `GET /api/auth/google` returns message |
| 8 | **Toggle password visibility** | Click eye icon next to password field | Password switches between visible/hidden | Frontend JS toggles `input.type` |
| 9 | **Auto-login on refresh** | Login successfully, refresh page | Dashboard loads automatically with fresh name from `/api/auth/me` | Validates token via `GET /api/auth/me`. If 401/403 → clears localStorage and shows login. |
| 10 | **Deactivated auto-login** | Login, then deactivate account from another session, refresh | Auto-logged out, login screen shown | `/api/auth/me` returns 403 → frontend clears state |

### Test Credentials (Seed Script Values)

| Role | Email | Password | First Login? |
|------|-------|----------|-------------|
| Agency | `admin@hashtagorang.in` | `Admin@123` | No |
| Client | `fenesta@example.com` | `Client@123` | Yes (forced change) |
| Dealer 1 (Gurugram) | `dealer.gurugram@fenesta.com` | `Dealer@123` | Yes (forced change) |
| Dealer 2 (Noida) | `dealer.noida@fenesta.com` | `Dealer@123` | Yes (forced change) |
| Dealer 3 (Jaipur) | `dealer.jaipur@fenesta.com` | `Dealer@123` | Yes (forced change) |

---

## Screen 2: Password Change Screen

### What's On Screen
- Lock icon "Change Password" heading
- "You must change your temporary password before continuing." subtitle
- Current Password input (with eye icon)
- New Password input (with eye icon)
- "Update Password" green button
- Red error message area

### Functions & How to Test

| # | Action | How to Test | Expected Result | Background Process |
|---|--------|------------|-----------------|-------------------|
| 1 | **Change password** | Enter current temp password + new password (must meet strength rules), submit | Success → redirects to role-appropriate dashboard | `POST /api/auth/change-password` → validates strength → verifies old password via bcrypt → hashes new → sets `needs_password_change=FALSE` |
| 2 | **Wrong current password** | Enter wrong current password | Red error: "Current password is incorrect" | Backend returns 400 |
| 3 | **Weak password** | Enter a short/simple new password | Red error with specific requirement failure | Backend validates: 8+ chars, uppercase, lowercase, digit, special char |
| 4 | **Server error** | Stop backend, try submit | Red error: "Server error" | `fetch()` throws |

> **Password Requirements**: Minimum 8 characters, at least one uppercase letter, one lowercase letter, one digit, and one special character (e.g., `NewPass@1`).

---

## Screen 3: Agency Dashboard (Role: `agency`)

### What's On Screen
- White header bar: "Agency Administration — Manage Clients & Internal Locations"
- Logout button (top right)
- **2 Tab buttons**: Brand Clients | My Location
- "Active Clients" section with table (default tab)
- Table columns: Client Name | Admin Email | Dealers | Status | Actions
- "+ Add Client" blue button
- **Add Client Modal** (hidden by default): Client Name, Admin Email, GBP Account ID fields
- **Temp Password Toast** (hidden): green notification showing generated password

### Functions & How to Test

| # | Action | How to Test | Expected Result | Background Process |
|---|--------|------------|-----------------|-------------------|
| 1 | **View client list** | Login as agency admin | Table shows Fenesta with email, dealer count (3), Active badge | `GET /api/agency/clients` -> SQL joins `clients` + `users` + subquery counts dealers |
| 2 | **Empty state** | Delete all clients from DB | Table shows: "No clients yet." | Empty array from API |
| 3 | **Add Client** | Click "+ Add Client" -> fill Name + Email -> click "Create Client" | Modal closes, green toast shows temp password (15s auto-dismiss), table refreshes | `POST /api/agency/clients` -> generates temp password -> bcrypt hash -> INSERT users + clients |
| 4 | **Add duplicate email** | Try adding a client with an existing email | Alert: "A user with email '...' already exists." | Returns 409 |
| 5 | **Deactivate client** | Click "Deactivate" on an Active client -> confirm | Status changes to Inactive, **all child dealers also deactivated** | `DELETE /api/agency/clients/{id}` -> sets `is_active=FALSE` on clients, users, AND all child dealers + their users |
| 6 | **Reactivate client** | Click "Reactivate" on an Inactive client -> confirm | Status changes to Active, **all child dealers also reactivated** | `PUT /api/agency/clients/{id}/reactivate` -> sets `is_active=TRUE` on everything |
| 7 | **View client dealers** | Click the "3 dealers" link in the table | Modal opens showing dealer names, cities, emails, and status | `GET /api/agency/clients/{id}/dealers` with ownership verification |
| 8 | **My Location tab** | Click "My Location" tab | Shows setup screen if not initialized, or metrics dashboard if initialized | `GET /api/agency/dashboard/my-location` |
| 9 | **Initialize My Location** | Click "Initialize Internal Tracking" on setup screen | Creates internal client+dealer records, populates metrics via background task | `POST /api/agency/my-location` |
| 10 | **Logout** | Click Logout | Returns to login screen | Clears `localStorage`, reloads page |

---

## Screen 4: Client Dashboard (Role: `client`)

### What's On Screen
- Header: "Welcome, {full_name}" + "Cumulative Brand Performance" + Logout button
- **4 Tab buttons**: Global Overview | Dealer Network | Add Dealer | HQ Location
- Body changes per tab

### Tab 4A: Global Overview

**Visible elements**: 8 KPI metric cards + trend chart. Aggregates ALL locations (Dealers + HQ).

| Card | Data Source Key | Color Accent |
|------|----------------|-------------|
| Total Views | `total_views` | Blue |
| Search Impressions | `search_impressions` | Indigo |
| Map Views | `map_views` | Purple |
| Website Clicks | `website_clicks` | Green |
| Calls Made | `calls_made` | Yellow |
| Direction Clicks | `direction_clicks` | Orange |
| Avg Rating | `weighted_avg_rating` + `total_reviews` | Red |
| Total Locations | `total_locations` | Cyan |

| # | Action | Expected Result | Background Process |
|---|--------|-----------------|-------------------|
| 1 | **View global metrics** | All 8 cards show aggregated numbers across ALL locations (dealers + HQ) | `GET /api/client/dashboard/global` → SUMs all `metrics_cache` rows for active dealers → computes weighted avg rating |
| 2 | **View trend chart** | Line chart showing 6-month historical data | Trend data pulled from `trend_cache` table |
| 3 | **No dealers** | All cards show 0 | Empty metrics returned, total_locations=0 |

### Tab 4B: Dealer Network

**Visible elements**: Sub-dealer table + aggregated network metrics (excluding HQ).

| # | Action | Expected Result | Background Process |
|---|--------|-----------------|-------------------|
| 1 | **View dealer list** | Shows all sub-dealers (NOT HQ) with Active/Inactive badges | `GET /api/client/dealers` → filters `WHERE user_id != current_user` |
| 2 | **Click "View Data"** | Modal opens with dealer metrics in card grid + 6-month trend chart | `GET /api/client/dashboard/dealer/{id}` → verifies ownership → returns metrics + trend_data |
| 3 | **Deactivate dealer** | Click "Disable" → confirm → status changes to Inactive | `DELETE /api/client/dealers/{id}` → `is_active=FALSE` on both dealers and users table |
| 4 | **Reactivate dealer** | Click "Enable" → confirm → status changes to Active | `PUT /api/client/dealers/{id}/reactivate` |
| 5 | **Network metrics** | Cards below table show aggregated metrics for sub-dealers only (excluding HQ) | `GET /api/client/dashboard/network` |
| 6 | **Dealer from another client** | 403: "Dealer does not belong to your account" | Ownership query fails |

### Tab 4C: Add Dealer

**Visible elements**: Form with fields: Dealer Name, Email, City, State, Google Location ID + "Create Dealer Account" button

| # | Action | Expected Result | Background Process |
|---|--------|-----------------|-------------------|
| 1 | **Add dealer** | Green message: "✅ Dealer 'X' created successfully. Temp password: {pwd}" | `POST /api/client/dealers` → generates temp password → INSERT users + dealers → triggers `sync_location_metrics` as background task |
| 2 | **Missing required fields** | Browser alert: "Name, Email, and Location ID are required." | Frontend validation (JS) |
| 3 | **Duplicate email** | Red error: "A user with email '...' already exists." | Returns 409 |

### Tab 4D: HQ Location

**Visible elements**: Setup form (if not linked) or HQ metrics dashboard (if linked).

| # | Action | Expected Result | Background Process |
|---|--------|-----------------|-------------------|
| 1 | **View HQ (not set up)** | Shows setup form: Name + Location ID fields | `GET /api/client/dashboard/hq` returns `status: "not_initialized"` |
| 2 | **Link HQ location** | Click "Link Location" → success alert → HQ dashboard loads | `POST /api/client/my-location` → uses `HqLocationRequest` model (no email needed) → creates dealer record tied to client's `user_id` |
| 3 | **View HQ metrics** | 8 KPI cards + 6-month trend chart specific to HQ location | `GET /api/client/dashboard/hq` returns metrics from `metrics_cache` + `trend_cache` |
| 4 | **Duplicate HQ** | Error: "HQ location already linked to this account" | Backend checks for existing record before INSERT |

---

## Screen 5: Dealer Dashboard (Role: `dealer`)

### What's On Screen
- Header: "Welcome, {full_name}" + "Live Google Business Profile Metrics"
- "📝 Publish Post" green button + Logout button
- **7 KPI cards** in a 4-column grid (last card spans 2 columns)
- **6-Month View Trend Analysis** line chart (Chart.js, from `trend_cache`)
- **Post Modal** (hidden by default)

| Card | Data Key | Border Color |
|------|----------|-------------|
| Total Views | `total_views` | Blue |
| Search Impressions | `search_impressions` | Indigo |
| Map Views | `map_views` | Purple |
| Website Clicks | `website_clicks` | Green |
| Calls Made | `calls_made` | Yellow |
| Direction Clicks | `direction_clicks` | Cyan |
| Average Rating + Reviews | `average_rating` + `total_reviews` | Red (spans 2 cols) |

### Functions & How to Test

| # | Action | Expected Result | Background Process |
|---|--------|-----------------|-------------------|
| 1 | **View own metrics** | 7 cards populated with numbers from DB | `GET /api/dealer/dashboard` → finds dealer record via `user_id` → fetches `metrics_cache` + `trend_cache` |
| 2 | **View trend chart** | Line chart with 6 months of historical data from `trend_cache` | Data comes from backend, NOT random JS |
| 3 | **Create Post** | Click "📝 Publish Post" → type content → "Publish Now" | `POST /api/dealer/posts/create` → **MOCK only** → returns success message |
| 4 | **Empty post** | Click publish with no text | Browser alert: "Please write something!" | Frontend validation |
| 5 | **Access other role's endpoints** | Try `GET /api/agency/clients` with dealer token | 403: "Operation not permitted for this role" | `require_role(["agency"])` middleware blocks |

---

## Database Schema

### Tables (5 total)

| Table | Purpose |
|-------|---------|
| `users` | All user accounts (agency, client, dealer roles) with auth fields |
| `clients` | Brand client records, linked to agency user and client user |
| `dealers` | Dealer/location records, linked to client and dealer user |
| `metrics_cache` | Current snapshot of GBP metrics per location |
| `trend_cache` | Monthly historical view counts per location (6-month history) |

---

## Security Features

| Feature | Status | Details |
|---------|--------|---------|
| JWT Authentication | ✅ | Token-based auth with configurable expiry |
| Role-Based Access Control | ✅ | `require_role()` middleware on all endpoints |
| Cascade Deactivation | ✅ | Disabling a client cascades to all child dealers |
| Parent-Active Check | ✅ | Dealers blocked if parent client is inactive |
| Password Strength Validation | ✅ | 8+ chars, uppercase, lowercase, digit, special char |
| XSS Protection | ✅ | `sanitize()` helper on all dynamic HTML injection |
| Duplicate Email Protection | ✅ | 409 error with user-friendly message |
| Soft Delete | ✅ | `is_active` flags, never hard deletes |

---

## Bug Fix Log (May 4, 2026)

| # | Bug | Severity | Fix Applied |
|---|-----|----------|-------------|
| 1 | `trend_cache` table missing from schema.sql and database.py | 🔴 Critical | Added `CREATE TABLE trend_cache` to both files |
| 2 | Agency `my-location` ON CONFLICT issues + missing `full_name` | 🔴 High | Explicit idempotent checks, `full_name` added to middleware return |
| 3 | Agency dashboard `agency-total-dealers` ID mismatch (should be `agency-total-reviews`) | 🟡 Medium | Fixed HTML element ID |
| 4 | Client deactivation doesn't cascade to dealers | 🟡 Medium | Added cascade UPDATE to `deactivate_client()` and `reactivate_client()` |
| 5 | Client `my-location` requires dummy email | 🟡 Medium | Created separate `HqLocationRequest` model without email |
| 6 | `database.py` dead `if not conn` guard | 🟡 Low | Removed dead code |
| 7 | No password strength validation | 🟠 Security | Added 5-rule validation in `change-password` endpoint |
| 8 | XSS via template literal injection | 🟠 Security | Added `sanitize()` helper, applied to all dynamic HTML |
| 9 | ~2000 lines of commented-out dead code | 📋 Cleanup | Removed from `client/routes.py`, `dealer/routes.py`, `sync.py`, `app.js` |
| 10 | `full_name` not returned by `get_current_user()` middleware | 📋 Cleanup | Added to query and return dict |

---

## Remaining Known Gaps (Phase 6 & 7)

| # | Gap | Priority | Phase |
|---|-----|----------|-------|
| 1 | Real Google Business Profile API integration | High | Phase 6 |
| 2 | Rate limiting on auth endpoints | High | Phase 6 |
| 3 | Email delivery of temp passwords | Medium | Phase 6 |
| 4 | `PUT /api/agency/clients/{id}` — edit client | Low | Phase 6 |
| 5 | Loading skeleton screens | Low | Phase 7 |
