# PLAN.md: Lead Pipeline Admin Portal

Standalone admin portal in PHP plus MySQL. Auth, staff management, account
settings. Full design in
`docs/superpowers/specs/2026-07-13-admin-portal-design.md`.

Rules that apply to every phase:
- PDO prepared statements only. No string-concatenated SQL.
- Tailwind via local npm build. No CDN.
- No inline CSS or JS. Separate files under `/assets`.
- Reusable PHP partials and helpers. No copy-paste.
- Responsive on mobile, tablet, desktop.
- No em-dash characters anywhere (code, UI, content, docs).
- Plain, human, functional copy.
- Rebuild and commit `assets/css/dist/output.css` whenever templates change, so
  the compiled CSS never drifts from the markup (`npm run build:css`).

## Phase 1: Scaffold plus Tailwind pipeline (DONE)

- [x] Create folder structure per the spec.
- [x] `package.json` with `build:css` and `watch:css` scripts.
- [x] `tailwind.config.js` scanning the PHP template directories.
- [x] `assets/css/input.css` with `@tailwind` directives and a components layer.
- [x] `npm install`, then run `build:css` and confirm
      `assets/css/dist/output.css` is generated.
- [x] `.gitignore` already excludes `node_modules`. output.css is committed so
      the app serves without a node build on the server.
- [x] Verify: output.css exists (base reset, 4754 bytes) and PHP class scanning
      picks up utilities, custom `brand` colors, and the `@apply` components.

## Phase 2: Helpers plus layout components (DONE)

- [x] `/helpers/bootstrap.php` requires all helpers and starts a hardened session
      (httponly, samesite Lax, idle timeout).
- [x] `/helpers`: `db.php`, `auth.php`, `csrf.php`, `flash.php`,
      `validation.php`, `response.php`, `password.php`, `pagination.php`, plus
      `view.php` (escaping and component/page rendering).
- [x] `/config`: `constants.php` and `config.sample.php` (real `config.php` is
      git-ignored).
- [x] `/includes`: `layout.php`, `header.php`, `sidebar.php`, `footer.php`, plus
      `shell.php` (bare shell for login, setup, and error pages).
- [x] `/components`: `button.php`, `alert.php`, `form_field.php`, `table.php`,
      `modal.php`, `card.php`, `pagination.php`.
- [x] `/assets/js`: `sidebar.js`, `menu.js`, `alerts.js`, `modal.js`,
      `confirm.js`, `form.js` (one concern each, no inline JS).
- [x] Throwaway harness rendered the layout plus every component once, then was
      deleted at the end of the phase.
- [x] Verify: harness returned HTTP 200 via `php -S`, all component markers
      present, assets served 200, no PHP notices. Confirmed no em-dash, no inline
      CSS, no inline JS. CSS rebuilt and committed.

## Phase 3: Auth plus schema (schema NOT executed) (DONE)

- [x] `/sql/schema.sql`: `users` (including `must_change_password`) and
      `login_attempts` tables per the spec. Written, NOT executed. Force-added to
      git since the repo ignores `*.sql` by default.
- [x] `index.php` front controller and route map with central guard.
- [x] `/setup/setup.php`: create first admin. Self-disables by checking the
      database for any existing admin row on every load (no flag file), with a
      re-check inside the POST to close the race.
- [x] `/auth/login.php`: email plus password, server-side validation, session
      login, session id regeneration on success.
- [x] DB-backed lockout: 5 fails per email plus IP in 15 minutes. Prune rows
      older than the window on each attempt. Clears on success. Locked accounts
      get the same generic message as a bad password (no enumeration).
- [x] Force change password on login when `must_change_password = 1`. User
      cannot reach other pages until the password is changed and the flag clears.
- [x] `/auth/logout.php`: destroy session, redirect to login.
- [x] `/auth/guard.php`: CSRF for every POST, auth check plus role check, 403 for
      wrong role.
- [x] Placeholder dashboard behind the guard.
- [x] Verify: with no live DB, confirmed via HTTP and a throwaway session seeder:
      404 on unknown route, login GET 200, dashboard-while-logged-out 302 to
      login, POST without CSRF 419 (blocked in guard before any DB call), staff
      hitting an admin route 403, admin 200, must_change 302 to change-password,
      logged-in user bounced off the login page. Scaffolding removed after.

## Phase 4: STOP for database details

- [ ] Pause. Ask the owner for DB connection details.
- [ ] Owner fills `/config/config.php` and runs `/sql/schema.sql`.
- [ ] Do not connect to the live database or run any migration before this.

## Phase 5: Staff management (admin only)

- [ ] `staff/list.php`: paginated list with name, email, role, status.
- [ ] `staff/create.php`: add staff, validation, unique email, admin-set
      temporary password (hashed), `must_change_password = 1`, temp password
      shown to the admin once on the result screen.
- [ ] `staff/edit.php`: edit name, email, role.
- [ ] `staff/toggle.php`: activate and deactivate (status flag), confirm modal.
- [ ] RBAC: staff cannot reach any of these routes (guard returns 403).
- [ ] Verify: drive each action end to end against the owner's DB.

## Phase 6: Account settings plus change password

- [ ] `admin/account.php`: update own name and email.
- [ ] `admin/change_password.php`: current password check, new plus confirm.
- [ ] Verify: change own profile and password, then log in with the new one.

## Phase 7: Final pass

- [ ] Responsive check at mobile, tablet, desktop breakpoints.
- [ ] Grep for inline `style=` and inline `<script>` bodies. None allowed.
- [ ] Grep for em-dash characters across the repo. None allowed.
- [ ] Confirm RBAC holds on every admin route.
- [ ] Confirm every query uses prepared statements.
