# Lead Pipeline Admin Portal (Phase 4, standalone)

Design spec. Date: 2026-07-13. Status: approved.

## Purpose

Build a standalone, production-ready admin portal in PHP and MySQL. This is the
admin panel layer (Phase 4) of a larger lead-aggregation system, but the current
scope is limited to authentication, staff management, and account settings. The
lead pipeline itself is out of scope for now. A placeholder dashboard stands in
for future Phase 4 content.

## Decisions (locked)

1. Login is by email only.
2. Failed logins are throttled with a database-backed lockout: 5 failed attempts
   per email plus IP address inside a 15 minute window triggers a lock until the
   window clears. Thresholds are config-overridable.
3. The first admin account is created through a one-time `/setup` page. The page
   self-disables by checking the database for any existing admin row on every
   load (no flag file).
4. After login, users land on a placeholder dashboard so the layout and
   navigation are demonstrable.
5. Profile holds name and email only (plus role, status, password, timestamps).
6. Staff removal is deactivate-only (a status flag). No hard delete.
7. Admin sets a temporary password when creating a staff member. The temp
   password is shown to the admin once at creation time (no mail layer). The new
   user has `must_change_password = 1` and is forced to change the password on
   first login before reaching any other page.

## Hard rules (carried from the brief)

- PHP plus MySQL. PDO with prepared statements for every query. No raw
  string-concatenated SQL.
- Tailwind CSS via the local build pipeline (npm plus Tailwind CLI). No CDN
  script. `tailwind.config.js`, `input.css` with `@tailwind` directives, and a
  build script in `package.json` that compiles to `assets/css/dist/output.css`.
- No inline CSS and no inline JavaScript. All CSS in `/assets/css`, all JS in
  `/assets/js`, referenced by `<link>` and `<script src>`.
- Reusable UI components as PHP partials (header, sidebar, footer, buttons, form
  fields, alerts, tables, modals, cards). Nothing copy-pasted.
- Reusable helper functions (DB connection, query wrappers, input sanitization,
  CSRF, flash messages, redirects, password hashing, auth checks, pagination) in
  `/helpers`, required centrally.
- Fully responsive (mobile, tablet, desktop) using Tailwind responsive
  utilities.
- Do not use the em-dash character anywhere, including code comments, UI text,
  content, this spec, and PLAN.md. Use commas, periods, or parentheses.
- UI and copy read like a real human-built internal tool. Plain, specific,
  functional labels. No marketing tone.
- Industry-standard layout: fixed and collapsible sidebar, top header with user
  menu, footer, consistent across all authenticated pages.

## Architecture

Plain PHP (no framework) plus MySQL through PDO. Front controller pattern: every
request enters through `index.php`, which loads config and helpers, starts the
session, then routes to a page script. Authenticated pages render inside one
shared layout (header, collapsible sidebar, footer). No Composer runtime
dependencies. The only build tooling is Tailwind via npm.

Request flow:

```
index.php
  -> bootstrap (config, constants, helpers, session)
  -> route map
  -> guard (auth check + role check)
  -> page script
  -> layout partials (header, sidebar, footer)
  -> HTML response
```

## Folder structure

```
/config        config.php (DB creds filled in by the owner), constants.php
/helpers       bootstrap.php, db.php, auth.php, csrf.php, flash.php,
               validation.php, response.php, password.php, pagination.php
/includes      layout.php (wrapper), header.php, sidebar.php, footer.php
/components     button.php, alert.php, form_field.php, table.php, modal.php,
               card.php, pagination.php
/auth          login.php, logout.php, guard.php
/admin         dashboard.php, account.php, change_password.php,
               staff/list.php, staff/create.php, staff/edit.php, staff/toggle.php
/setup         setup.php (self-disabling first-admin bootstrap)
/assets/css    input.css, dist/output.css (compiled, build output)
/assets/js     sidebar.js, confirm.js, form.js
/sql           schema.sql (NOT executed until the owner confirms)
index.php      front controller / router
package.json, tailwind.config.js
```

## Database schema (`/sql/schema.sql`, not executed)

### users
- `id` INT PK auto increment
- `name` VARCHAR
- `email` VARCHAR, unique
- `password_hash` VARCHAR
- `role` ENUM('admin','staff')
- `status` ENUM('active','inactive'), default 'active'
- `must_change_password` TINYINT (0 or 1), default 0
- `created_at`, `updated_at` timestamps

Login is by email. A row with `status = 'inactive'` cannot log in. When
`must_change_password = 1`, the user is forced to the change-password screen on
login and cannot reach any other page until the password is changed, at which
point the flag is cleared.

### login_attempts
- `id` INT PK auto increment
- `email` VARCHAR
- `ip_address` VARCHAR
- `attempted_at` timestamp
- `successful` TINYINT (0 or 1)

Lockout logic: count failed rows for (email plus IP) inside the last 15 minutes.
If the count is 5 or more, the account is locked until the window clears.
Successful login clears that account plus IP pair. On every login attempt the
portal also prunes rows older than the lockout window so the table does not grow
unbounded. A scheduled cron job can take over pruning later if needed.

Seeding: no seeded admin row. `/setup` creates the first admin.

## Security model

- PDO prepared statements everywhere. `ERRMODE_EXCEPTION`. Emulated prepares
  turned off.
- CSRF: one token per session, hidden field on every POST form, validated with
  `hash_equals` before any POST is processed.
- Passwords: `password_hash()` (bcrypt default) and `password_verify()`. Change
  password requires the current password.
- Sessions: cookies set `httponly` and `samesite=Lax`. Session id regenerated on
  login. Idle timeout default 60 minutes, set in config.
- RBAC: `guard.php` accepts a required role. Staff hitting an admin-only route
  get a 403 page, not a redirect loop.
- Lockout: 5 failed attempts per email plus IP inside 15 minutes, config
  overridable.

## Roles and access

- admin: dashboard, full staff management (list, add, edit, activate,
  deactivate, assign role), own account, own change password, logout.
- staff: dashboard, own account, own change password, logout. No staff
  management routes.
- The sidebar renders links conditionally by role, and the guard enforces access
  server-side. The hidden menu is never trusted for enforcement.

## Tailwind pipeline

`package.json` scripts: `build:css` (one-shot minified) and `watch:css` (dev
watch). `input.css` holds the three `@tailwind` directives plus a small
`@layer components` for shared button and input classes. `tailwind.config.js`
scans `./**/*.php` for class names. Output compiles to
`assets/css/dist/output.css`. The compile is verified before any UI is built.
The CDN script is not used anywhere.

## Phasing

1. Scaffold plus Tailwind. Folders, `package.json`, config compiles to
   `output.css` (verified).
2. Helpers plus layout components. All of `/helpers`, `/includes`, and
   `/components`, with a throwaway harness page to confirm they render.
3. Auth plus schema. `/setup`, login, logout, guard, lockout, and `schema.sql`
   written but not executed.
4. STOP. Ask the owner for DB details. The owner runs the schema.
5. Staff management. List (paginated), add, edit, activate, deactivate, assign
   role.
6. Account settings plus change password.
7. Final pass. Responsive check across breakpoints, confirm no inline CSS or JS,
   no em-dashes, RBAC holds.

Each phase ends in a working, verifiable state.

## Out of scope (for now)

- The lead pipeline and lead data.
- Profile avatar uploads and file handling.
- Remember-me and server-side session storage table.
- Hard delete of staff records.
- Email sending (password reset by email, notifications).
