# HerbaCommunity — local setup run book

Getting the whole thing running on your PC. Roughly 30–40 minutes, most of it
waiting for downloads.

> **Status check before you start:** the front-end prototype is working and
> tested. The Laravel code is written but **has never been executed** — there was
> no PHP on the machine when it was authored. Step 5 is the first real test of
> it, and it is normal to hit a couple of small fixes there.

---

## 1. Install the runtime

### Linux (recommended — this is a server stack)

Ubuntu / Debian:

```bash
sudo apt update
sudo apt install -y php8.2 php8.2-cli php8.2-mysql php8.2-mbstring \
                    php8.2-xml php8.2-curl php8.2-zip php8.2-bcmath \
                    mysql-server composer git unzip
```

Fedora / RHEL: `sudo dnf install php php-mysqlnd php-mbstring php-xml php-json mariadb-server composer git`

Arch: `sudo pacman -S php php-gd mariadb composer git`

Start MySQL and confirm the toolchain:

```bash
sudo systemctl enable --now mysql     # or mariadb
php -v && composer -V && mysql --version
```

> The `mbstring`, `xml`, `curl` and `bcmath` extensions are not optional —
> Laravel will fail at `composer install` without them. `bcmath` matters here
> specifically because recharge amounts are DECIMAL.

**Docker alternative** — if you would rather not install PHP system-wide,
[Laravel Sail](https://laravel.com/docs/sail) gives you PHP + MySQL in
containers with `./vendor/bin/sail up`. Only Docker is needed on the host.

### Windows

**Laragon** — https://laragon.org/download (Full edition bundles PHP 8.2+,
MySQL, Composer and Git). Alternatives: Herd (no MySQL on the free tier), or
XAMPP + Composer by hand.

---

### Linux gotchas that will bite

These are real and cost time if you hit them cold:

1. **The filesystem is case-sensitive.** `App\Models\BfaRecord` MUST live at
   `app/Models/BfaRecord.php` — exact case. On Windows a mismatch silently
   works; on Linux you get "class not found". `tests/test-php-structure.js`
   checks this case-sensitively and currently passes.
2. **Storage permissions.** After `composer create-project`:
   ```bash
   chmod -R 775 storage bootstrap/cache
   sudo chown -R $USER:www-data storage bootstrap/cache
   ```
   Skipping this gives a blank white page with nothing useful in the browser.
3. **MySQL root auth.** Modern MySQL uses `auth_socket` for root, so a blank
   password from PHP fails. Either run `sudo mysql` to create a dedicated user:
   ```sql
   CREATE USER 'herba'@'localhost' IDENTIFIED BY 'a-real-password';
   GRANT ALL ON herbacommunity.* TO 'herba'@'localhost';
   ```
   and use that in `.env`, or set a root password.
4. **Line endings.** If the project came from Windows, `git config core.autocrlf input`
   avoids stray `\r` in shell scripts.

---

## 2. Create the Laravel project

```bash
cd <the HerbaCommunity folder>     # wherever you copied it
composer create-project laravel/laravel herbacommunity-api
cd herbacommunity-api
php artisan install:api            # Laravel 11+: adds Sanctum + routes/api.php
```

`create-project` always pulls the current stable release, so no version is
hardcoded anywhere.

---

## 3. Copy the application code in

From `HerbaCommunity\api\`, copy over the new project preserving paths:

| From `api\` | To `herbacommunity-api\` |
|---|---|
| `app\Models\*` | `app\Models\` |
| `app\Models\Concerns\*` | `app\Models\Concerns\` |
| `app\Scopes\*` | `app\Scopes\` |
| `app\Services\*` | `app\Services\` |
| `app\Policies\*` | `app\Policies\` |
| `app\Http\Controllers\Api\*` | `app\Http\Controllers\Api\` |
| `app\Http\Middleware\SetActiveBranch.php` | `app\Http\Middleware\` |
| `database\migrations\*` | `database\migrations\` |
| `database\seeders\*` | `database\seeders\` (overwrite `DatabaseSeeder.php`) |
| `routes\api.php` | `routes\` (overwrite) |

Then **delete the skeleton's own** `database\migrations\0001_01_01_000000_create_users_table.php`
and `0001_01_01_000001_create_cache_table.php` — ours replace them.

Two small edits are also needed — both are described in `api\INTEGRATION.md`:

1. Register the `active.branch` middleware alias and enable sessions on API
   routes, in `bootstrap\app.php`.
2. Add `AuthorizesRequests` to the base `App\Http\Controllers\Controller`
   (it is an empty class on Laravel 11+).

---

## 4. Database

In Laragon, MySQL is already running. Create the database:

```bash
mysql -u root -e "CREATE DATABASE herbacommunity CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
```

Edit `.env`:

```dotenv
APP_NAME=HerbaCommunity
APP_URL=http://localhost:8000

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=herbacommunity
DB_USERNAME=root
DB_PASSWORD=

SESSION_DRIVER=database

# Set to false to skip the fake members/guests and get a clean network only
DEMO_DATA=true
```

---

## 5. Migrate and seed — the first real test

```bash
php artisan migrate
php artisan db:seed
```

**Expect to fix a thing or two here.** Known candidates, all noted in
`INTEGRATION.md`:

- `Branch::nextCardId()` uses MySQL's `SUBSTRING` — fine on MySQL, breaks on SQLite.
- `UniqueConstraintViolationException` needs Laravel 10.x or newer.
- The `RIGHT(REPLACE(...))` phone expression in `PersonMatchService` is MySQL syntax.

Send me the error output and I'll fix it properly rather than guessing.

What you should end up with:

```
8 clubs · ~145 members · ~53 guests · 4 deliberate repeat guests
```

---

## 6. Run it

```bash
php artisan serve
```

API at `http://localhost:8000/api`. Quick smoke test:

```bash
curl http://localhost:8000/api/login-options
```

---

## 7. Front-end + PWA

Copy into `herbacommunity-api\public\`:

```
pwa\manifest.json      →  public\manifest.json
pwa\sw.js              →  public\sw.js
pwa\offline.html       →  public\offline.html
pwa\icons\             →  public\icons\
herbalife-dashboard.html → public\index.html
```

Then paste the two blocks from `pwa\pwa-head.html` into `public\index.html`
(one before the closing head tag, one before the closing body tag).

Open `http://localhost:8000`.

> **PWA needs HTTPS** — except on `localhost`, which browsers treat as secure.
> So install/offline works locally out of the box, but when you move to a real
> server you must have a TLS certificate or the service worker will not register.

### Testing install

- **Desktop Chrome/Edge** — an install icon appears in the address bar, and the
  "Install app" button shows bottom-right.
- **Android Chrome** — menu ▸ Install app.
- **iPhone Safari** — Share ▸ Add to Home Screen. iOS never fires
  `beforeinstallprompt`, so the app shows a one-time hint instead of a button.

To verify offline behaviour: DevTools ▸ Application ▸ Service Workers ▸ tick
**Offline**, then reload. You should get the app shell, not the browser error page.

---

## 8. Before real use — non-negotiable

- [ ] **Change every PIN.** Seeded demo PINs (123456 / 654321 / 000000 / 111111
      / 222222 / 333333 / 444444) are printed by the seeder as a warning.
- [ ] **Re-seed without demo data** for go-live:
      `php artisan migrate:fresh --seed --seeder=NetworkSeeder`
      then import the real member list. The 145 demo members are fabricated.
- [ ] `APP_DEBUG=false` and a real `APP_KEY` in production.
- [ ] HTTPS, or the PWA will not install and tokens travel in clear.
- [ ] Decide a backup schedule for the database. Member data and revenue history
      have no other copy.
- [ ] Test the recharge maths against real prices before trusting the collection
      report with money.

---

## What is built vs what is not

**Working and tested**
- Front-end prototype, 9,072 lines — club pyramid, downline rollups, role-gated
  visibility, network-wide repeat-guest detection, plus everything that was there
  before (scans, recharges, tiers, referral fuel, reports, CSV import).

**Written, not yet executed**
- Laravel: 21 tables, 19 models, 3 policies, branch scoping, 6 controllers,
  2 seeders.
- PWA: manifest, service worker with offline queue, 6 icons, offline page.

**Written and tested against a mocked backend**
- The API layer in the dashboard (`HC` object, mappers, hydration, overrides).
  Tested with a stubbed `fetch`: demo fallback, debouncing, the 409 repeat flow,
  scan error cases, scope refusal and in-place hydration all verified. It has
  **not** been run against the real Laravel API yet.

## How the front-end talks to the API

The dashboard auto-detects the backend at boot (`GET /api/login-options`):

- **Backend reachable** → API mode. Login posts to `/api/login`, and every
  screen is fed by data fetched from the server.
- **Backend unreachable** → demo mode, with an orange banner reading
  *"DEMO MODE — no backend connected."* Everything still works off the seeded
  demo data, so the app stays demonstrable on a laptop with nothing running.

The trick that made this tractable: the ~300 render functions all read the same
global arrays (`M`, `GUESTS`, `BRANCHES`…). Rather than rewriting them, the API
layer **refills those arrays in place** — the render layer never learns where
the data came from. Same technique as the branch scoping.

Writes go through the API and patch the local arrays from the response. The
server is authoritative for everything that matters.

### Endpoints used

| Action | Call |
|---|---|
| Detect backend / login list | `GET /login-options` |
| Login / resume / logout | `POST /login`, `GET /me`, `POST /logout` |
| Hydrate | `GET /branches`, `/members`, `/guests`, `/scans/today` |
| Repeat-guest check (debounced 350 ms) | `POST /guests/check-duplicate` |
| Log a guest | `POST /guests` (retries with `acknowledge_repeat`) |
| Scan | `POST /scans` |
| Register | `POST /members` (retries with `acknowledge_duplicate`) |
| Switch club / scope | `POST /branches/{id}/operate-as`, `POST /scope` |
| Open a club | `POST /branches` |

Every request carries `X-Branch-Id` and `X-Scope-Mode`. The server validates
both against the user's own subtree — a tampered header just falls back.
