# Uploading this build to GoDaddy

Target: `https://www.dghanalytics.com/aiseo`
Server folder: `public_html/aiseo/`

This is a **full replacement of the application folder**. Your `.env`, your
database and your audit history are not in the zip and are not touched.

Total time: about ten minutes, and about four of those are the upload.

---

## Before you start — one thing to check

Open cPanel → File Manager → `public_html/aiseo/` and confirm `.env` is there.
(Turn on *Settings → Show hidden files* if you cannot see it.)

**The zip does not contain `.env`.** If you delete the folder rather than
overwrite it, you lose your database credentials. Do not delete the folder.

---

## 1. Back up what you have (2 minutes)

In File Manager, select the `aiseo` folder → **Compress** → zip →
`aiseo-backup-2026-08-19.zip`. Leave it in `public_html`.

If anything goes wrong you extract that over the top and you are back where you
started. This is worth the two minutes.

---

## 2. Upload

1. File Manager → open `public_html/aiseo/`
2. **Upload** → choose `aiseo-3.0.zip`
3. When it finishes, go back to `public_html/aiseo/`, right-click the zip →
   **Extract** → extract into `public_html/aiseo`
4. Say **yes** to overwriting. Nothing outside this folder is affected, and
   `.env` is not in the archive so it survives.
5. Delete the zip from the server.

Check the structure is right. `public_html/aiseo/` should contain
`bootstrap.php`, and folders `public/`, `src/`, `templates/`, `db/`, `config/`,
`bin/`, `var/`. If you instead see a nested `aiseo/aiseo/`, move the contents up
one level.

---

## 3. Run the migration

Open:

```
https://www.dghanalytics.com/aiseo/
```

You will see **"migrations outstanding"** rather than the dashboard. That is
expected — this build adds the analysis and content tables, and they do not exist
yet.

If `migrate-web.php` is missing from your server (you may have deleted it after
the last install), it is in this zip, so it is back.

1. Click **Open migrate-web.php**
2. Tick the confirmation box → **Run migrations**
3. It should report `009_settings_content.sql` applied — plus `007` and `008`
   if you are coming from build 2.x, and `006_users.sql` from build 1.x.
   Anything already recorded is skipped, and it says so.

`007` **creates new tables only** (`site_country`, `analysis_run`,
`keyword_gap`, `content_calendar`, `article`, `article_revision`,
`article_image`, `cms_connection`, `publish_attempt`). It alters nothing that
exists and touches no audit data.

`008` adds four columns to `job`, `publish_attempt` and `article`, and releases
stale rows so a new unique index can be created. Every statement checks whether
its change is already present, so running it twice is safe.

`009` **creates new tables** (`site_settings`, `site_document`, `site_offering`,
`site_internal_link`, `article_feedback`, `backlink_snapshot`, `backlink_domain`,
`gsc_performance`) and adds four columns to `article` and one to `competitor`.
Every column addition is guarded by an `information_schema` check rather than
`ADD COLUMN IF NOT EXISTS`, which is MariaDB-only — so this runs on MySQL too,
and running it twice is a no-op. It seeds one settings row per existing website,
so the Article settings screen works the moment the migration finishes.

Nothing in `009` alters or deletes an existing column, so your audit history,
articles and publishing connections are untouched.

Both were run against a real MariaDB before release — on a clean database and on
a copy of the exact half-upgraded state this step could otherwise have hit.

---

## 4. Create your administrator account

Go back to:

```
https://www.dghanalytics.com/aiseo/
```

You get a **"Create the first administrator"** screen. Fill in your name, your
email address and a password of at least ten characters.

This screen appears exactly once. The moment an account exists it is replaced by
the normal sign-in form, so nobody can use it later to add themselves as an
administrator.

Use a passphrase — three or four words — rather than a mangled single word. It is
both stronger and easier to type on a phone.

---

## 5. Delete the three installer files

This is the step that is easy to skip and should not be.

In File Manager, delete from `public_html/aiseo/`:

```
setup-env.php     migrate-web.php     preflight.php
```

`migrate-web.php` has a **Delete this file** button at the bottom of its own
page, and it now refuses anonymous visitors once an account exists. The other two
cannot be gated — `setup-env.php` can rewrite your database credentials and
`preflight.php` prints server paths and the tail of your error log to anybody who
asks for it. Both are meant to work when the rest of the app does not, which is
exactly why they cannot be put behind the login.

The app will show an orange banner naming the files until they are gone.

---

## 6. Add the people who need access

Sidebar → **People**.

| Role | Can |
|---|---|
| Administrator | Everything, including managing accounts |
| Analyst | Run audits, mark findings fixed, connect integrations |
| Viewer | Read only — every button that changes something is hidden, *and* refused by the server if called directly |

Give directors **Viewer**. Give whoever runs the audits **Analyst**. Keep
administrators to two people, so one locked account never locks everyone out.

You set the temporary password yourself and pass it on in person or over a
channel you trust. The app sends no email, on purpose — there is no reset inbox
for anyone to intercept.

---

## 7. Two things that were outstanding before this build

Both are unchanged by this upload and both are still worth doing.

**a. Fix `APP_URL`.** Edit `.env` in File Manager:

```
APP_URL=https://www.dghanalytics.com/aiseo
```

Every page works without this — the app derives its own path from the filesystem
and shows a yellow banner about the mismatch — but the Google OAuth redirect URI
is built from `APP_URL` and cannot self-correct.

**b. Install the worker cron.** This is no longer optional in the way it was.
Audits were the only queued work before; now the analysis, the article drafting,
the image generation **and the publishing** are all queued too. Without a worker
every one of those buttons accepts the click and nothing ever happens.

cPanel → Cron Jobs → every 5 minutes:

```
cd ~/public_html/aiseo && /usr/local/bin/php bin/worker.php --max-jobs=20 >> var/logs/cron.log 2>&1
```

Use whichever PHP path the cPanel Cron Jobs page suggests if it differs. Until
this exists, the dashboard says *"nothing is consuming the queue — is the worker
cron running?"*, and any article left mid-job for ten minutes shows a banner
saying the same thing with a button to get it unstuck.

---

## 8. Add the three API keys

Nothing in the content pipeline works without these. Edit `.env` in File Manager
and fill in:

```
DATAFORSEO_LOGIN=            # your email
DATAFORSEO_PASSWORD=
DATAFORSEO_DAILY_BUDGET_USD=15.00

ANTHROPIC_API_KEY=           # writes the articles
ANTHROPIC_DAILY_BUDGET_USD=10.00

OPENAI_API_KEY=              # generates the hero images
OPENAI_DAILY_BUDGET_USD=5.00
```

The three budgets are **hard stops**, not warnings. A run that hits one stops and
says so — it does not report an empty result and leave you guessing. Raise them
before a large first run rather than during one; one country/language pair costs
roughly a dollar to analyse, an article a few cents, an image four cents.

Then: **Analyze** → tick your markets → **Load every country** if the list is
empty (that endpoint is free) → **Save markets** → **Analyze**.
The worker picks it up within five minutes.

---

## 9. Connect each site's CMS

Sidebar → **Publishing**. One card per site. Pick where it publishes, fill in the
credentials, and press **Save and test the connection** — the test costs nothing
and it is how you find a wrong URL or an expired password without sending a real
article to find out. **An untested connection cannot publish**; that is enforced,
not advised.

- **WordPress** — the site URL, a username, and an Application Password
  (WP Admin → Users → Profile → Application Passwords; *not* the login password).
- **Shopify** — the `.myshopify.com` domain and a custom-app access token with
  `write_content` and `write_files`.
- **Wix** — an API key and site id. Wix creates **drafts only**; its API has no
  publish path, and the cover image is added in the Wix editor.
- **Your own PHP or static site** — the receiver code is printed at the bottom
  of the Publishing page. About thirty lines, with a bearer token and an HMAC
  signature over the body.
- **Squarespace** — cannot be published to by anything, including this. Its API
  keys cover Commerce, Orders and Inventory; there is no blog permission to
  grant. Those articles come out as a **Download bundle** — the HTML, the
  JSON-LD and the image, ready to paste in.

Credentials are encrypted with AES-256-GCM before they reach the database and are
never displayed again — only the last four characters, so you can tell one from
another. This needs `APP_KEY` to be set; the page says so if it is not.

**Start with "Create it as a draft".** A first publish that lands as a draft is a
reversible mistake. One that goes straight live is not.

---

## If something goes wrong

**A blank page or a 500.** Re-upload `preflight.php` from the zip and open
`https://www.dghanalytics.com/aiseo/preflight.php`. It reports the PHP version,
missing extensions, resolved folders, whether every source file parses, whether
`.env` is readable *by PHP specifically*, and the tail of the error log — then
gives a ranked verdict. Delete it again afterwards.

**"One migration outstanding" will not go away.** The migration did not commit.
Open `migrate-web.php` again; it lists what is pending and what is already
recorded.

**You are locked out of every account.** There is no back door by design. Fix it
in phpMyAdmin — select the `dghanalytics_aiseo` database and run:

```sql
DELETE FROM app_user;
```

Then reload the app: with the table empty it offers the "create the first
administrator" screen again. Your sites, runs and findings are in different
tables and are not affected.

**An article is stuck on "drafting" or "publishing".** After ten minutes the
article page says so and offers a button to move it out of that state. The cause
is almost always the worker cron not running — check `var/logs/cron.log`.

**"Publishing is blocked to stop a duplicate".** An earlier attempt reached the
CMS but this app never got the answer, so it cannot tell whether the post exists.
Open the live site and look. If the article is there, nothing more is needed. If
it is not, press *"I checked — the article is not on the site"* and publish
again. This is deliberate: the alternative is posting the same article to a
customer-facing website twice.

**An analysis says "inconclusive".** Expand the row on the Analyze screen — it
says which of the three reasons it was: the budget cap stopped it, DataForSEO
returned no competing domains for that market, or every competitor found was
classified as a directory. The third is fixable on the Competitors screen, and
your reclassification is never overwritten by a later run.

**Roll back.** Extract `aiseo-backup-2026-08-19.zip` over the folder. The new
tables can stay — the previous build ignores them.
