# Insourcehire — Hosted Entry System (setup & operations)

Two internal forms on your site generate your unique pages automatically, protect every
data-collecting surface from bots, and preserve ALL data to Google Sheets forever.
Generated HTML pages auto-expire after **90 days**; the sheets keep everything.

## Architecture

```
site/entry-system/           ← internal (access code + Turnstile + rate limits)
  candidate-entry.php        → generates a unique candidate upload page
  client-entry.php           → generates a unique client shortlist page (single or expanded)
  verify_action.php          → gates actions ON generated pages (bot check + rate limit)
  expire_pages.php           → cron sweep: deletes pages older than 90 days, purges state
  apps_script_bridge.gs      → Google Apps Script: appends every submission to your sheets
  lib/, config.php
site/candidate-pages/<token>/index.html   ← generated pages (unique URLs, self-expiring)
site/client-pages/<token>/index.html
site/ih-state/               ← sqlite (rate limits + meta) — NOT web-accessible
site/ih-templates/           ← the locked MASTER templates (single source of truth)
```

## Setup — step by step

1. **Upload** the `entry-system/` folder into `public_html/` on cPanel (File Manager or FTP).
2. **Create the folders** `public_html/candidate-pages`, `public_html/client-pages`,
   `public_html/ih-state`, `public_html/ih-templates`. Set `ih-state` to **755** and confirm
   the web server user can write to it (chown to the site user if needed).
3. **Copy the locked masters** into `public_html/ih-templates/`:
   `insourcehire_shortlist_landing_template.html`, `insourcehire_shortlist_UK_expanded_template.html`,
   `insourcehire_candidate_instruction_page_template.html`. From now on, page redesigns happen
   here (they contain the bot-protection snippet with `{{ENTRY_BASE}}` / `{{TURNSTILE_SITE_KEY}}`).
4. **Turnstile keys**: dash.cloudflare.com → Turnstile → Add site → domain `insourcehire.com`
   → copy **site key** and **secret key** → paste into `config.php` (`IH_TS_SITE_KEY`,
   `IH_TS_SECRET_KEY`) and set `IH_TS_SKIP_VERIFY` to `false` once testing is done.
5. **Access code**: run `php -r "echo password_hash('CHOOSE_A_SECRET', PASSWORD_DEFAULT);"`
   and paste the hash into `config.php` (`IH_ACCESS_HASH`). Share the code only with your team.
6. **Entry base**: set `IH_ENTRY_BASE` to the absolute URL of the entry-system folder,
   e.g. `https://insourcehire.com/entry-system`. This activates protection on generated pages.
7. **Sheets bridge** (keeps every submission forever):
   1. In Google Sheets, open (or create) a spreadsheet with two tabs named exactly
      `CandidateData` and `ClientData` (headers: `timestamp | kind | token | url | expires | data`).
   2. Extensions → Apps Script → paste `apps_script_bridge.gs` → set `SPREADSHEET_ID`
      (the id after `/d/` in the sheet URL) → save → Deploy → Web app →
      *Execute as: Me · Who has access: Anyone* → copy the `/exec` URL.
   3. Paste it into `config.php` as `IH_SHEETS_CANDIDATE_URL` and `IH_SHEETS_CLIENT_URL`.
8. **Cron (auto-expiry)**: cPanel → Cron Jobs →
   `0 3 * * * php /home/USER/public_html/entry-system/expire_pages.php`
   (adjust the home path). Pages older than 90 days are deleted nightly; sheets keep the data.
9. **Protect `ih-state`**: add an `.htaccess` inside `ih-state` containing `Require all denied`
   (or `Order Allow,Deny / Deny from all`) so the sqlite file is never downloadable.

## The protection matrix

| Surface | Access code | CAPTCHA | Rate limit | Honeypot |
|---|---|---|---|---|
| Internal candidate-entry form | ✅ | Turnstile | 24 pages/day global + 10/hr/IP | ✅ |
| Internal client-entry form | ✅ | Turnstile | 6 pages/day global + 10/hr/IP | ✅ |
| Generated candidate page (Upload → Google Form) | — | Turnstile (invisible) | 30/hr/IP | ✅ (in form) |
| Generated client page (Arrange/let's connect) | — | Turnstile (invisible) | 20/hr/IP | ✅ |

## What happens on submit (candidate-entry)

1. Access code → honeypot → CAPTCHA → rate limits (reject with a plain message, no page created)
2. Validate → build page from the master → write `candidate-pages/<token>/index.html`
3. Push the full record (token, URL, expiry, **all fields**) to the CandidateData sheet (and local meta)
4. Respond with the unique URL + expiry date

## Operational notes

- **Four knobs in `config.php`** cover everything: caps, expiry days, keys, sheet URLs.
- **Fail-open rule**: if Turnstile keys are still placeholders (`REPLACE_...`), generated pages
  let clicks through with a console warning (so a misconfigured deploy never blocks conversions)
  — keep `IH_TS_SKIP_VERIFY=false` and real keys in production.
- **90-day expiry** per founder decision (prospects may return to old comms); change
  `IH_EXPIRY_DAYS` any time — old pages are cleaned by cron.
- **Volume assumptions**: ≤24 candidate pages/day, ≤6 client pages/day. Raise the two caps in
  `config.php` if the team grows.
- **Single-variant corridors** are chosen by the fx pair at generation time: UK+India
  (INR/GBP) · UK+SA (ZAR/GBP) · SA+India (INR/ZAR). The expanded variant is always UK client, 3+3.
- **Offline fallback**: the JSON fill-sheet + `generate_page.py` path still works anywhere —
  the entry forms are the hosted convenience on top of the same masters.

## Testing checklist (after deploy)

- [ ] Both internal forms load; wrong access code rejected; honeypot submission rejected
- [ ] A candidate page generates with a unique `/candidate-pages/<token>/` URL
- [ ] A single + an expanded client page generate; cost totals match your QA numbers
- [ ] Generated pages open; with `IH_ENTRY_BASE` set, the Arrange/Upload actions require a
      Turnstile pass and obey the hourly caps
- [ ] Submissions appear in the CandidateData / ClientData tabs
- [ ] `expire_pages.php` run manually deletes an expired test page and purges state
- [ ] `php entry-system/tests.php` reports all PASS
