# Deploying to Turbify (or any no-SSH/no-Composer shared host)

This guide is for hosting with **no SSH, no Composer, no artisan CLI, and possibly no
cron** — everything is done through file upload (FTP / File Manager) and a browser.

Everything you need is in the shipped ZIP: **`AI-Vision-Attendance-Turbify.zip`**
(vendor/ already installed, no Composer needed). A standalone copy of the preflight
checker, **`check.php`**, is also provided outside the zip so you can upload/test it
before committing to the full upload.

---

## 0. Two possible docroot situations — read this first

Turbify (and many shared hosts) can be set up two different ways. Figure out which one
you have **before** uploading, because it changes where files go.

### Scenario A — you CAN point the domain/subdomain docroot at a `public/` folder (preferred)

Some Turbify plans let you choose an arbitrary folder as the document root for a
domain/subdomain. If so:

```
/home/yoursite/app/                 <- everything EXCEPT the public/ folder's contents
    app/, bootstrap/, config/, database/, resources/, routes/, storage/,
    vendor/, artisan, composer.json, .env, ...
    public/                         <- upload this folder's CONTENTS as the docroot
        index.php
        .htaccess
        check.php
        css/
```

Set the docroot to `/home/yoursite/app/public`. No further changes needed —
`public/index.php` already resolves the app root as `__DIR__.'/../'` relative to
itself, which is correct as long as `public/` stays one level directly under the app
folder (which it does either way; only the *docroot pointer* changes between the two
scenarios).

**This is the recommended scenario** — simpler, and keeps all application code (and
`.env`, which contains DB credentials) outside the web-servable tree entirely.

### Scenario B — the docroot is FIXED (e.g. you only get `public_html/` and can't repoint it)

If your plan gives you a fixed web root you cannot change, split the upload like this:

```
/home/yoursite/av-app/              <- upload everything EXCEPT the public/ folder here
    app/, bootstrap/, config/, database/, resources/, routes/, storage/,
    vendor/, artisan, composer.json, .env, ...

/home/yoursite/public_html/         <- your fixed docroot; upload ONLY the CONTENTS
                                        of public/ here (index.php, .htaccess, css/, check.php)
```

Then edit **`public_html/index.php`** (the two lines that reference `__DIR__.'/../'`)
to point at the real app location instead:

```php
// Register the Composer autoloader...
require '/home/yoursite/av-app/vendor/autoload.php';

// Bootstrap Laravel and handle the request...
/** @var Application $app */
$app = require_once '/home/yoursite/av-app/bootstrap/app.php';
```

(Use the **absolute filesystem path** your host gives you for your home directory —
check their docs/support for the exact path; it's often shown in the cPanel/File
Manager "home directory" field.) Also check `storage/framework/maintenance.php`
reference at the top of `index.php` and update that path the same way if present.

If you're unsure which scenario applies, contact Turbify support and ask "can I set a
custom document root / docroot for this domain, or is it fixed to public_html?" — this
is the one Turbify-specific detail this guide cannot answer for you in advance.

---

## 1. Upload order

1. **Upload `check.php` FIRST**, standalone, into whatever will become your docroot.
   Browse to `https://yourdomain/check.php` and confirm every row is **PASS** (PHP
   version ≥ 8.2, required extensions, storage/bootstrap-cache writable, .htaccess
   present). Fix anything red before proceeding — usually a support ticket to enable
   a PHP extension, or a chmod via File Manager.
2. **Unzip and upload the rest of `AI-Vision-Attendance-Turbify.zip`** per whichever
   docroot scenario (§0) applies. `vendor/` is already inside the zip — no Composer
   needed on the server.
3. Make sure `storage/` and all its subfolders, plus `bootstrap/cache/`, are writable
   by the web server user (chmod 755 or 775 recursively via your File Manager's
   permissions tool if the host doesn't do this automatically on upload).

---

## 2. Create the MySQL database in Turbify's control panel

The install wizard **cannot create a database for you** (shared hosts virtually never
allow that from a plain PHP/MySQL user) — you must create it first:

1. In Turbify's control panel, find **MySQL Databases** (naming varies by panel).
2. Create a new database (note the exact name — Turbify commonly prefixes it with
   your account name, e.g. `youraccount_avattend`).
3. Create a MySQL user and **assign it to that database with full privileges**.
4. Note the **DB host** Turbify gives you — it is often `localhost`, but some shared
   hosts use a different internal hostname; check their MySQL documentation/panel.
5. Keep the host/database/username/password handy for the next step.

---

## 3. Run `/install`

Browse to `https://yourdomain/install`. If the app can't boot at all (rare — the
shipped `.env` is deliberately minimal so this should not happen; see §6), you'll see
a 500 — re-check `check.php` and folder permissions first.

The wizard is a single page styled as a WordPress-style stepped installer — a
branded card with a numbered step indicator at the top, one step visible at a time
(vanilla JS; if JS is disabled it falls back to one long scrollable form with every
field visible, so nothing is ever unreachable). The actual install still happens in
**one atomic server request** when you click the final **Install** button — nothing
about the multi-step UI changes that; the steps are purely a front-end presentation
of the same single POST.

1. **Welcome** — product name/description and a "Let's get started" button.
2. **Requirements** — the same preflight checks re-run live inside Laravel (PHP
   version, required extensions, `storage/`/`bootstrap/cache` writable), shown as
   PASS/FAIL rows with a **Re-check** button. You cannot continue past this step
   until every row passes.
3. **Database** — host/port/name/username/password. A **Test connection** button
   calls a small AJAX endpoint (`POST /install/test-db`) and shows an inline green
   "✓ Connected" or red "✗ &lt;reason&gt;" message, exactly like WordPress's DB step —
   you cannot continue until a test has passed. (This is a convenience only; the
   server re-validates the same connection again on final submit regardless, so
   JS-disabled users are not blocked, just without the live inline check.)
4. **Site & admin** — application name, the full public URL of your site (e.g.
   `https://yourdomain.com`, prefilled from the address you used to reach the page),
   how many sites/devices to provision (default 5, matches the pilot's 5-site design
   — change if you're doing a smaller pilot), and the admin name/email/password
   (with a confirm field and a non-blocking strength hint).
5. **Review & install** — a read-only summary of everything you entered (password
   shown masked) and the **Install** button. Clicking it shows a spinner/"Installing…"
   overlay while the single POST request runs, then the browser navigates to the
   success screen.

On submit, in that one request the wizard: writes a fresh `.env` (production
settings, freshly generated `APP_KEY`), runs all migrations, and seeds **only**
production-essential data — the admin user, the requested number of sites with **1
device each and a freshly generated token**, and default app settings. It
**deliberately does not create any sample/demo employees** (unlike the dev seeder used
during development, which adds 10 fake `EMP0001`–`EMP0010` rows for testing).

If `.env` can't be written (app root not writable by PHP), the wizard stops **before**
touching the database and shows you the exact file content to save manually via
FTP/File Manager — then you resubmit the form.

### Save the device tokens — shown ONCE

The completion screen shows a table of `site → device_id → token` and a **"Download
tokens (.txt)"** button. These tokens are **never written to disk on the server** —
if you close the tab without saving them, generate replacement devices manually later
(there's no way to recover a lost token, only reissue a new one — this is by design,
same as the API contract's "tokens hashed at rest" rule). Copy each site's token into
that site's edge device config (`edge/config.yaml`):

```yaml
central_url: "https://yourdomain.com"   # your app URL, /api/v1 is appended automatically
device_token: "<the 64-char token for that site>"
```

Re-running `/install` after it has already completed is blocked — you'll see an
"already installed" page instead (see §7).

---

## 4. External cron (no-cron accommodations)

This app is designed to need **zero cron jobs** for correctness:

- The dashboard and daily reports **always compute on the fly** from raw
  `attendance_events` on every page load — nothing depends on a nightly batch job
  having run.
- There are no queued jobs (`QUEUE_CONNECTION=sync`) — nothing to run a worker for.

Two housekeeping tasks are still nice to automate if your host allows *any* outbound
scheduling (even a free third-party pinger), each exposed as a **token-protected GET
URL** (token minted at install time, shown on the completion screen and again on the
admin **Maintenance** page after login):

```
https://yourdomain.com/tasks/purge-snapshots?token=<token>&days=60
https://yourdomain.com/tasks/compute-attendance?token=<token>       # precomputes yesterday, as a safety net
```

If Turbify gives you `cron` in the control panel, add a daily `curl`/wget hit of both
URLs there. If not, use a free external pinger such as
**[cron-job.org](https://cron-job.org)** — create a free account, add each URL as a
scheduled job (once daily is plenty), done. Both endpoints are plain `GET` and return
JSON; they do real work synchronously and return once finished (fine for these fast
operations at pilot scale).

The admin **Maintenance** page (`/maintenance`, admin-only) also has a manual "Purge
snapshots older than N days" button if you'd rather trigger it by hand than schedule
anything.

---

## 5. Security note — delete `check.php` after install

`check.php` reveals PHP version/extension/path details to anyone who requests it.
Once `/install` has completed and you've confirmed the app works, **delete
`check.php` from the server** (FTP/File Manager). It has no other function and is not
referenced anywhere else in the app.

---

## 6. How "boots with no configured DB" works (for the curious / for troubleshooting)

The zip ships a **minimal placeholder `.env`** (see `.env.turbify.example` in the
source repo for the template) with:

- A **valid, freshly generated `APP_KEY`** baked in at zip-build time — Laravel 11+
  needs a real key just to encrypt cookies on *every* request (even `/install`
  itself), so without this the very first page load would 500 before you ever saw the
  wizard. The wizard **replaces this key with a brand new one** the moment install
  succeeds, so the placeholder never protects real production data.
- `SESSION_DRIVER=file`, `CACHE_STORE=file`, `QUEUE_CONNECTION=sync` — so nothing
  before (or after) install ever needs a working database connection just to render a
  page or keep you logged in. (The dev environment for this project uses
  `SESSION_DRIVER=database` instead; production intentionally uses `file` sessions
  here for simplicity/robustness on shared hosting — one less moving part, no session
  table to babysit, works identically on hosts with strict MySQL connection limits.)
- `DB_CONNECTION=mysql` pointing at an intentionally-invalid placeholder database
  name, so if anything ever *did* try to query the database before `/install` runs, it
  fails loudly (clear error) instead of silently.

`App\Http\Middleware\EnsureInstalled` (web) / `EnsureApiInstalled` (API) check for
`storage/app/installed.lock` on every request; until it exists, every web route
redirects to `/install` and every API route returns a clean
`{"error":{"code":"not_installed", ...}}` JSON response (edge devices can't follow a
browser redirect, hence the different behavior for `/api/*`).

---

## 7. Reinstalling / starting over

`/install` will refuse to run again once `storage/app/installed.lock` exists — you'll
see an "already installed" page instead of a form (this is deliberate: the wizard
never silently wipes an existing install). To force a genuine from-scratch reinstall:

1. Back up anything you need from the current database.
2. Delete `storage/app/installed.lock` via FTP/File Manager.
3. Reload `/install` — the form will appear again. Note it will **re-run migrations
   and re-seed** against whatever DB credentials you enter, so point it at a fresh (or
   emptied) database unless you want a clean slate on the existing one.

---

## 8. Assumptions / things this guide couldn't verify directly (flag for the user)

- **Turbify's exact MySQL host string, database-naming convention, and whether custom
  docroots are configurable** vary by plan and were not directly testable outside a
  real Turbify account — §0 covers both possibilities and §2 gives generic
  cPanel-style steps; confirm the specifics against your actual Turbify control panel
  or their support docs.
- **Cron availability** on Turbify's current shared plans is unconfirmed — §4's
  external-pinger fallback (cron-job.org) works regardless of whether Turbify offers
  cron, so it's the safe default; use real cron instead if your plan has it.
- **PHP version/extensions available by default** on Turbify were not directly
  verifiable — `check.php` (§1 step 1) is the actual source of truth for your account;
  read its results, not this document, if the two ever disagree.
- **mod_rewrite/.htaccess overrides**: most Apache-based shared hosts (which Turbify
  is) honour `.htaccess` by default, but if you get raw 404s instead of the Laravel
  app at `/install`, ask Turbify support to confirm `AllowOverride All` (or mod_rewrite
  specifically) is enabled for your domain.
