# WMS

# System Update

# How it Works

<p id="bkmrk-project-forge-wms-te" class="align-left">Project Forge WMS technical and operational architecture</p>
<h2 id="bkmrk-1.-purpose-and-deplo">1. Purpose and Deployment Model</h2>
<p id="bkmrk-system-update-instal">System Update installs an approved Project Forge WMS GitHub Release on a staging or production server. It downloads a packaged, checksummed release. It does not run <code spellcheck="false"><span class="editor-theme-code">git pull</span></code>, require a Git working tree, copy staging files to production, or invoke Plesk Git deployment.</p>
<p id="bkmrk-developer-%E2%86%92-git-tag-"><strong>Developer → Git tag → GitHub Release → Staging → Production</strong></p>
<p id="bkmrk-staging-and-producti">Staging and production independently download the same immutable release assets from GitHub.</p>
<h2 id="bkmrk-2.-major-components">2. Major Components</h2>
<table id="bkmrk-componentresponsibil"><colgroup><col><col></colgroup><tbody><tr><th><p>Component</p></th><th><p>Responsibility</p></th></tr><tr><td><p><code spellcheck="false"><span class="editor-theme-code">httpdocs/manifest.json</span></code></p></td><td><p>Application version, numeric build, requirements, database version, release date, and release notes.</p></td></tr><tr><td><p><code spellcheck="false"><span class="editor-theme-code">.github/workflows/project-forge-release.yml</span></code></p></td><td><p>Validates the tag, builds the ZIP, calculates SHA-256, and creates a GitHub prerelease.</p></td></tr><tr><td><p><code spellcheck="false"><span class="editor-theme-code">httpdocs/modules/system_update/</span></code></p></td><td><p>Administrator UI, AJAX endpoints, release discovery, validation, installation, backup, and status.</p></td></tr><tr><td><p><code spellcheck="false"><span class="editor-theme-code">ScheduleScripts/system_update.php</span></code></p></td><td><p>CLI worker that claims and executes one queued update job.</p></td></tr><tr><td><p><code spellcheck="false"><span class="editor-theme-code">system_jobs</span></code></p></td><td><p>Stores queued, running, completed, and failed jobs.</p></td></tr><tr><td><p><code spellcheck="false"><span class="editor-theme-code">APP_ROOT/storage/system_updates/</span></code></p></td><td><p>Private downloads, extraction, backups, locks, logs, and installed-file inventory.</p></td></tr><tr><td><p><code spellcheck="false"><span class="editor-theme-code">APP_ROOT/config/system_update.php</span></code></p></td><td><p>Private channel and GitHub token shared by web PHP and the CLI worker.</p></td></tr></tbody></table>
<h2 id="bkmrk-3.-release-channels-">3. Release Channels and Immutability</h2>
<table id="bkmrk-channeleligible-rele"><colgroup><col><col><col></colgroup><tbody><tr><th><p>Channel</p></th><th><p>Eligible releases</p></th><th><p>Use</p></th></tr><tr><td><p><code spellcheck="false"><span class="editor-theme-code">staging</span></code></p></td><td><p>Non-draft prereleases and stable releases</p></td><td><p>Test before production approval.</p></td></tr><tr><td><p><code spellcheck="false"><span class="editor-theme-code">production</span></code></p></td><td><p>Non-draft stable releases only</p></td><td><p>Install explicitly promoted releases.</p></td></tr></tbody></table>
<p id="bkmrk-every-new-tag-is-pub">Every new tag is published as a prerelease. After staging installs and approves it, an operator edits the same GitHub Release and clears its prerelease flag. Promotion must not create another tag, rebuild the ZIP, replace an asset, or change the checksum.</p>
<h2 id="bkmrk-4.-check-for-updates">4. Check for Updates</h2>
<ol id="bkmrk-the-browser-calls-mo"><li value="1">The browser calls <code spellcheck="false"><span class="editor-theme-code">modules/system_update/ajax/check_update.php</span></code>.</li><li value="2">The endpoint validates the local manifest.</li><li value="3">The GitHub Releases API is queried using the private token.</li><li value="4">Drafts and releases disallowed by the configured channel are removed.</li><li value="5">The updater requires an exactly named ZIP and SHA-256 asset.</li><li value="6">The remote manifest is validated and compared with the local manifest.</li><li value="7">Update is enabled only when a valid newer release is identified.</li></ol>
<p id="bkmrk-a-release-is-newer-w">A release is newer when its numeric build is greater, or when builds tie and its semantic version is greater.</p>
<p id="bkmrk-read-only%3A-check-for"><strong>Read-only:</strong> Check for Updates does not modify files or queue installation.</p>
<h2 id="bkmrk-5.-queue%2C-monitor%2C-a">5. Queue, Monitor, and Execute</h2>
<ol id="bkmrk-ajax%2Fstart_update.ph"><li value="1"><code spellcheck="false"><span class="editor-theme-code">ajax/start_update.php</span></code> validates POST, authorization, CSRF, tag, channel, and concurrency.</li><li value="2">It creates a <code spellcheck="false"><span class="editor-theme-code">system_update</span></code> job with status <code spellcheck="false"><span class="editor-theme-code">queue</span></code>.</li><li value="3">The browser polls <code spellcheck="false"><span class="editor-theme-code">job_status.php</span></code>. Polling only reads status; it does not execute the job.</li><li value="4">Plesk runs <code spellcheck="false"><span class="editor-theme-code">ScheduleScripts/system_update.php</span></code>.</li><li value="5">The worker atomically claims the job and executes the update service.</li></ol>
<p id="bkmrk-plesk%3A-use-scheduled"><strong>Plesk:</strong> use Scheduled Task type <em>Run a PHP script</em>, not <em>Run a command</em>. Command tasks are chrooted and may not contain PHP.</p>
<pre id="bkmrk-script%3A-schedulescri"><code>Script: ScheduleScripts/system_update.php
PHP: 8.5
Schedule: * * * * *</code></pre>
<h2 id="bkmrk-6.-installation-phas">6. Installation Phases</h2>
<table id="bkmrk-phaseoperationdownlo"><colgroup><col><col></colgroup><tbody><tr><th><p>Phase</p></th><th><p>Operation</p></th></tr><tr><td><p>download</p></td><td><p>Resolve metadata and download ZIP/checksum using authenticated GitHub asset API URLs.</p></td></tr><tr><td><p>verify</p></td><td><p>Compare ZIP SHA-256 with the published checksum.</p></td></tr><tr><td><p>extract</p></td><td><p>Reject traversal, absolute paths, unsafe links, and size/count violations; extract privately.</p></td></tr><tr><td><p>preflight</p></td><td><p>Validate runtime, protected paths, storage placement, package layout, disk space, and migrations.</p></td></tr><tr><td><p>backup</p></td><td><p>Back up managed overwritten/removed files and record new files.</p></td></tr><tr><td><p>install</p></td><td><p>Overlay managed files and remove only obsolete files proven by a trusted inventory.</p></td></tr><tr><td><p>migrate</p></td><td><p>Run pending SQL migrations when present.</p></td></tr><tr><td><p>post-update</p></td><td><p>Verify required files and installed manifest; write new inventory.</p></td></tr><tr><td><p>cleanup</p></td><td><p>Remove temporary data and release the lock.</p></td></tr><tr><td><p>complete / failed</p></td><td><p>Record terminal state and administrator message.</p></td></tr></tbody></table>
<h2 id="bkmrk-7.-security-controls">7. Security Controls</h2>
<ul id="bkmrk-token-and-channel-ar"><li value="1">Token and channel are stored outside <code spellcheck="false"><span class="editor-theme-code">httpdocs</span></code> and never returned to the browser.</li><li value="2">Updater storage must resolve outside <code spellcheck="false"><span class="editor-theme-code">DOCUMENT_ROOT</span></code>.</li><li value="3">Downloads require HTTPS, approved GitHub hosts, authenticated asset API URLs, and approved redirect hosts.</li><li value="4">SHA-256 is verified before extraction.</li><li value="5">CSRF is mandatory for the update POST.</li><li value="6">Atomic job claiming and a lock prevent concurrent installation.</li><li value="7">Configuration, uploads, attachments, environment files, storage, logs, and backups are protected.</li></ul>
<p id="bkmrk-the-private-configur">The private configuration file returns this array:</p>
<pre id="bkmrk-return-%5B-%27channel%27-%3D"><code>return [
    'channel' =&gt; 'staging', // production on production
    'github_token' =&gt; 'TOKEN_VALUE',
];</code></pre>
<p id="bkmrk-recommended-private-">Recommended private-config mode: <code spellcheck="false"><span class="editor-theme-code">0640</span></code>, owned by the website user and subscription group.</p>
<h2 id="bkmrk-8.-storage-and-logs">8. Storage and Logs</h2>
<pre id="bkmrk-app_root%2Fconfig%2Fsyst"><code>APP_ROOT/config/system_update.php
APP_ROOT/storage/system_updates/downloads/
APP_ROOT/storage/system_updates/extracted/
APP_ROOT/storage/system_updates/backups/
APP_ROOT/storage/system_updates/locks/
APP_ROOT/storage/system_updates/logs/
APP_ROOT/ScheduleScripts/system_update.php</code></pre>
<p id="bkmrk-job-logs-use-storage">Job logs use <code spellcheck="false"><span class="editor-theme-code">storage/system_updates/logs/{job_id}.log</span></code>.</p>
<h2 id="bkmrk-9.-backup-and-recove">9. Backup and Recovery Limits</h2>
<p id="bkmrk-before-migrations-st">Before migrations start, rollback attempts to restore overwritten files, remove newly created managed files, and restore obsolete managed files removed during installation.</p>
<p id="bkmrk-database%3A-updater-fi"><strong>Database:</strong> updater file backups are not database dumps. SQL migrations are not automatically rolled back and may partially apply. A verified external database backup and recovery plan are required before migrations.</p>
<h2 id="bkmrk-10.-definition-of-su">10. Definition of Success</h2>
<ul id="bkmrk-job-status-is-done-a"><li value="1">Job status is <code spellcheck="false"><span class="editor-theme-code">done</span></code> and phase is <code spellcheck="false"><span class="editor-theme-code">complete</span></code>.</li><li value="2">Local manifest equals the installed release version and build.</li><li value="3">A new check no longer offers the installed release.</li><li value="4">Protected paths and private configuration remain unchanged.</li><li value="5">Critical WMS smoke tests pass.</li><li value="6">The installed artifact checksum equals the approved GitHub checksum.</li></ul>

# Developer Release and Deployment Guide

Local development → repository → staging → testing → production

## 1. Approved Release Flow

1. Develop and test changes locally.
2. Commit source changes to `<span class="editor-theme-code">main</span>`.
3. Update `<span class="editor-theme-code">manifest.json</span>` and commit the release.
4. Push a final version tag.
5. GitHub Actions builds one immutable ZIP/checksum pair as a prerelease.
6. Staging independently installs and tests that prerelease.
7. Promote the same GitHub Release by clearing only its prerelease flag.
8. Production independently installs the same asset.

**Never:** copy files from staging to production, rebuild during promotion, create a second production tag for the same release, or use Plesk Git deployment to install a release being tested through System Update.

## 2. New Server Installation

Complete this section once for every new staging or production installation. The updater cannot install its own missing foundation, so the initial application and System Update files must be deployed through Plesk Git.

### 2.1 Register the Plesk SSH deploy key

1. In Plesk, open the domain and create a Git repository from a remote repository.
2. Use repository URL `<span class="editor-theme-code">git@github.com:Andrew374e1/ProjectForge-WMS.git</span>`.
3. Create or choose a Plesk SSH key.
4. Copy the complete value shown under **SSH public key content**.
5. In the GitHub repository, open **Settings**, **Deploy keys**, and **Add deploy key**.
6. Use an environment-specific title such as `<span class="editor-theme-code">Project Forge WMS - Production Plesk</span>`.
7. Paste the public key and leave write access disabled.
8. Return to Plesk and finish creating the repository.

**Key security:** register only the public key. Never copy, publish, or commit the corresponding private key. Use separate deploy keys for staging and production.

### 2.2 Configure the Plesk repository

<table id="bkmrk-repository-typeremot"><colgroup><col></col><col></col></colgroup><tbody><tr><th>Repository type

</th><td>Remote repository

</td></tr><tr><th>Active branch

</th><td>`<span class="editor-theme-code">main</span>`

</td></tr><tr><th>Deployment path

</th><td>`<span class="editor-theme-code">/</span>`

</td></tr><tr><th>Deployment mode

</th><td>**Manual**

</td></tr><tr><th>Post-deploy actions

</th><td>Disabled

</td></tr></tbody></table>

Fetch the repository and perform one controlled manual deployment to install the application and updater foundation. Keep deployment mode manual afterward. Routine releases must be installed through System Update.

### 2.3 Configure PHP in Plesk

1. In Plesk, open the environment domain.
2. Open **PHP Settings**.
3. Enable PHP support.
4. Use the tested PHP version, currently `<span class="editor-theme-code">8.5</span>`. The application manifest requires PHP `<span class="editor-theme-code">8.3</span>` or newer.
5. Set **Run PHP as** to **FPM application served by nginx**.
6. Set `<span class="editor-theme-code">short_open_tag</span>` to `<span class="editor-theme-code">on</span>`.
7. Confirm the `<span class="editor-theme-code">curl</span>`, `<span class="editor-theme-code">mysqli</span>`, and `<span class="editor-theme-code">zip</span>` extensions are available.
8. Apply the PHP settings before testing login or System Update.

**Required handler:** do not use a different PHP handler for this installation. Incorrect handler settings can produce different routing behavior, including POST requests being redirected to extensionless GET requests.

```
sudo plesk bin domain --show-php-settings ENVIRONMENT_DOMAIN |
  grep -iE 'short_open_tag|open_basedir'
```

Replace `<span class="editor-theme-code">ENVIRONMENT_DOMAIN</span>` with the exact staging or production domain. The output must show `<span class="editor-theme-code">short_open_tag = on</span>`.

### 2.4 Create the application database configuration

Create the environment-specific application configuration in the `<span class="editor-theme-code">APP_ROOT/httpdocs/includes</span>` directory. The filename is `<span class="editor-theme-code">config.php</span>`. This file supplies the database connection used by the website, login, background jobs, integrations, and System Update.

The configuration must define the values expected by the existing WMS bootstrap:

```
define('DB_HOST', 'DATABASE_HOST');
define('DB_USER', 'DATABASE_USERNAME');
define('DB_PASS', 'DATABASE_PASSWORD');
define('DB_NAME', 'DATABASE_NAME');
```

- Replace every placeholder with the credentials for that environment.
- Production must use the production database. It must never point to the staging database.
- Staging must use the staging database.
- Never commit this file or include its contents in documentation, tickets, chat, screenshots, or logs.
- Keep the file protected from release replacement and managed-file deletion.
- Recommended file mode is `<span class="editor-theme-code">0640</span>`, readable by the website user and its subscription group.

```
APP_ROOT=/var/www/vhosts/ENVIRONMENT_DOMAIN
SITE_USER=$(stat -c '%U' "$APP_ROOT/httpdocs")
SITE_GROUP=$(stat -c '%G' "$APP_ROOT/httpdocs")
CONFIG_DIR="$APP_ROOT/httpdocs/includes"
DB_CONFIG=$(printf '%s/%s' "$CONFIG_DIR" "config.php")

sudo chown "$SITE_USER:$SITE_GROUP" "$DB_CONFIG"
sudo chmod 0640 "$DB_CONFIG"
```

**Credential verification:** confirm the database host and database name before login testing. Do not print or expose the database password.

### 2.5 Create a GitHub token for System Update

Create a fine-grained personal access token for each environment. This token is separate from the Plesk SSH deploy key: Plesk Git uses SSH for the initial repository deployment, while System Update uses the token to read private GitHub Releases and download their assets.

[![GitHub fine-grained token configured for Project Forge production with repository Contents read-only access](https://kbase.thethreelogistics.com/uploads/images/gallery/2026-07/scaled-1680-/3jZimage.png)](https://kbase.thethreelogistics.com/uploads/images/gallery/2026-07/3jZimage.png)

The example above shows the production token limited to the Project Forge repository, with Contents and required Metadata permissions set to read-only. Click the image to open the full-size version.

1. Sign in to the GitHub account that can read `<span class="editor-theme-code">Andrew374e1/ProjectForge-WMS</span>`.
2. Open the profile menu, then **Settings**.
3. Open **Developer settings**.
4. Open **Personal access tokens**, then **Fine-grained tokens**.
5. Click **Generate new token**.
6. Enter an environment-specific name such as `<span class="editor-theme-code">Project Forge WMS - Production Updater</span>`.
7. Set an expiration date that follows company credential-rotation policy.
8. Set the resource owner to `<span class="editor-theme-code">Andrew374e1</span>`.
9. Limit repository access to `<span class="editor-theme-code">ProjectForge-WMS</span>` only.
10. Under repository permissions, set **Contents** to **Read-only**. GitHub supplies required metadata read access automatically.
11. Leave every unrelated permission at its default no-access value.
12. Generate the token and copy it immediately. GitHub displays the complete token only once.
13. Store it directly in the environment's private `<span class="editor-theme-code">config/system_update.php</span>` file.

**Token security:** never paste a real token into kbase, tickets, chat, terminal history, screenshots, application logs, or Git. Use separate staging and production tokens so either environment can be revoked independently.

Official reference: [Managing personal access tokens on GitHub](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens).

#### Validate the token without displaying it

```
read -rsp "GitHub token: " GITHUB_TOKEN
echo

curl --silent --show-error \
  --header "Accept: application/vnd.github+json" \
  --header "Authorization: Bearer $GITHUB_TOKEN" \
  --header "X-GitHub-Api-Version: 2022-11-28" \
  --write-out "HTTP_STATUS=%{http_code}\n" \
  https://api.github.com/repos/Andrew374e1/ProjectForge-WMS

unset GITHUB_TOKEN
```

A valid token returns repository metadata and `<span class="editor-theme-code">HTTP_STATUS=200</span>`. A private repository commonly returns `<span class="editor-theme-code">404</span>` when the token is invalid, expired, assigned to the wrong owner, or missing repository access.

### 2.6 Create the private channel configuration

Create the file outside `<span class="editor-theme-code">httpdocs</span>`.

<table id="bkmrk-environmentexact-pat"><colgroup><col></col><col></col><col></col></colgroup><tbody><tr><th>Environment

</th><th>Exact path

</th><th>Channel

</th></tr><tr><td>Staging

</td><td>`<span class="editor-theme-code">/var/www/vhosts/staging.wms.thethreelogistics.com/config/system_update.php</span>`

</td><td>`<span class="editor-theme-code">staging</span>`

</td></tr><tr><td>Production

</td><td>`<span class="editor-theme-code">/var/www/vhosts/analytics.thethreelogistics.com/config/system_update.php</span>`

</td><td>`<span class="editor-theme-code">production</span>`

</td></tr></tbody></table>

The staging file returns this array:

```
return [
    'channel' => 'staging',
    'github_token' => 'STAGING_GITHUB_TOKEN',
];
```

The production file returns this array:

```
return [
    'channel' => 'production',
    'github_token' => 'PRODUCTION_GITHUB_TOKEN',
];
```

- Use a fine-grained token limited to `<span class="editor-theme-code">Andrew374e1/ProjectForge-WMS</span>` with Contents read access.
- Never place the token under `<span class="editor-theme-code">httpdocs</span>` or commit it to Git.
- Set file mode `<span class="editor-theme-code">0640</span>` and ownership that allows web PHP and the scheduled worker to read it.
- A valid `<span class="editor-theme-code">PROJECT_FORGE_UPDATE_CHANNEL</span>` environment value takes precedence over the private file.
- The hostname, Git branch, Plesk repository, and manifest do not determine the channel.

**Release filtering:** staging accepts prereleases and stable releases. Production rejects drafts and prereleases and accepts stable releases only.

### 2.7 Create private updater storage

```
APP_ROOT=/var/www/vhosts/ENVIRONMENT_DOMAIN
DOCROOT="$APP_ROOT/httpdocs"
UPDATE_STORAGE="$APP_ROOT/storage/system_updates"
SITE_USER=$(stat -c '%U' "$DOCROOT")
SITE_GROUP=$(stat -c '%G' "$DOCROOT")

sudo install -d -o "$SITE_USER" -g "$SITE_GROUP" -m 0750 \
  "$APP_ROOT/storage" \
  "$UPDATE_STORAGE" \
  "$UPDATE_STORAGE/downloads" \
  "$UPDATE_STORAGE/extracted" \
  "$UPDATE_STORAGE/backups" \
  "$UPDATE_STORAGE/locks" \
  "$UPDATE_STORAGE/logs"
```

Replace `<span class="editor-theme-code">ENVIRONMENT_DOMAIN</span>` with the exact staging or production domain. Storage must remain outside `<span class="editor-theme-code">httpdocs</span>`.

### 2.8 Configure the scheduled worker

- In Plesk Scheduled Tasks, use task type **Run a PHP script**.
- Script path: `<span class="editor-theme-code">schedulescripts/system_update.php</span>`.
- PHP version: `<span class="editor-theme-code">8.5</span>`.
- Schedule: every minute.
- Run as the website subscription user.

**Do not use Run a command.** The Plesk chroot command environment may not contain the required PHP executable.

### 2.9 Verify the installation

- Confirm the System Update page displays the correct environment channel.
- Confirm PHP runs as an FPM application served by nginx and `<span class="editor-theme-code">short_open_tag</span>` is on.
- Confirm the application database configuration points to the correct environment database.
- Confirm the private configuration and updater storage resolve outside `<span class="editor-theme-code">httpdocs</span>`.
- Confirm the website user can write to updater storage.
- Confirm PHP provides `<span class="editor-theme-code">curl</span>`, `<span class="editor-theme-code">mysqli</span>`, and `<span class="editor-theme-code">zip</span>`.
- Run the scheduled task once and confirm it exits normally when no job is queued.
- Keep Plesk Git deployment mode set to manual.

## 3. One-Time Server Requirements

<table id="bkmrk-requirementstagingpr"><colgroup><col></col><col></col><col></col></colgroup><tbody><tr><th>Requirement

</th><th>Staging

</th><th>Production

</th></tr><tr><td>Channel

</td><td>`<span class="editor-theme-code">staging</span>`

</td><td>`<span class="editor-theme-code">production</span>`

</td></tr><tr><td>GitHub token

</td><td colspan="2">Fine-grained private-repository token with Contents read access.

</td></tr><tr><td>PHP

</td><td colspan="2">PHP 8.3+; tested with Plesk PHP 8.5; FPM application served by nginx;

`<span class="editor-theme-code">short_open_tag = on</span>`

;

`<span class="editor-theme-code">curl</span>`

,

`<span class="editor-theme-code">mysqli</span>`

, and

`<span class="editor-theme-code">zip</span>`

.

</td></tr><tr><td>Database config

</td><td colspan="2">Directory:

`<span class="editor-theme-code">APP_ROOT/httpdocs/includes</span>`

. Filename:

`<span class="editor-theme-code">config.php</span>`

. It must be environment-specific, protected, and excluded from releases.

</td></tr><tr><td>Private config

</td><td colspan="2">`<span class="editor-theme-code">APP_ROOT/config/system_update.php</span>`

, outside

`<span class="editor-theme-code">httpdocs</span>`

, mode

`<span class="editor-theme-code">0640</span>`

.

</td></tr><tr><td>Private storage

</td><td colspan="2">`<span class="editor-theme-code">APP_ROOT/storage/system_updates</span>`

, outside

`<span class="editor-theme-code">httpdocs</span>`

, writable by website user.

</td></tr><tr><td>Plesk Git

</td><td colspan="2">Manual deployment mode.

</td></tr><tr><td>Worker

</td><td colspan="2">Plesk

**Run a PHP script**

;

`<span class="editor-theme-code">schedulescripts/system_update.php</span>`

; PHP 8.5; every minute.

</td></tr></tbody></table>

## 4. Manifest and Version Requirements

- Use semantic versions, for example `<span class="editor-theme-code">1.0.6</span>`.
- Increase the numeric `<span class="editor-theme-code">build</span>` for every release.
- The tag must be `<span class="editor-theme-code">v</span>` plus the exact manifest version.
- Set release date and administrator-readable release notes.
- Change `<span class="editor-theme-code">database_version</span>` only when adding migrations.
- Never edit a published release manifest or move an installed tag.

```
{
  "application": "Project Forge WMS",
  "version": "1.0.6",
  "build": 6,
  "release_date": "2026-07-21",
  "minimum_php": "8.3",
  "minimum_mariadb": "11.8",
  "database_version": "20260717_001",
  "release_notes": [
    "Describe the administrator-visible change.",
    "Describe important operational or security behavior."
  ]
}
```

## 5. Local Developer Commands

### 5.1 Start with a clean branch

```
cd C:\Development\wms
git status --short
git branch --show-current
git pull --ff-only
```

Use `<span class="editor-theme-code">main</span>`. Resolve unrelated changes before release preparation.

### 5.2 Review edits

```
git status --short
git diff
git diff -- path\to\specific-file.php
```

### 5.3 Validate

```
C:\xampp\php\php.exe -l path\to\changed-file.php
C:\xampp\php\php.exe httpdocs\modules\system_update\tests\system_update_tests.php
```

Syntax-check every changed PHP file and require all relevant tests to pass.

### 5.4 Add, review, commit, and push development changes

```
git add path\to\changed-file.php path\to\another-file.js
git status --short
git diff --cached
git commit -m "Describe the implemented change"
git push origin main
```

**Use explicit paths.** Review staged changes before committing and preserve unrelated work.

## 6. Prepare and Publish the Release

### 6.1 Commit the manifest

```
git diff -- httpdocs/manifest.json
git add httpdocs/manifest.json
git diff --cached
git commit -m "Release v1.0.6"
git push origin main
```

### 6.2 Verify before tagging

```
git status --short
git log -2 --oneline
```

The worktree must be clean and the release commit must already appear on `<span class="editor-theme-code">origin/main</span>`.

### 6.3 Tag the release commit

```
git tag -a v1.0.6 -m "Project Forge WMS v1.0.6"
git push origin v1.0.6
git show v1.0.6 --no-patch --format=fuller
```

### 6.4 Verify GitHub Actions and assets

The release workflow must succeed and create a prerelease with exactly:

```
project-forge-wms-v1.0.6.zip
project-forge-wms-v1.0.6.sha256
```

If the workflow fails, fix the workflow or code; do not manually create an incomplete Release.

## 7. Correcting an Unpublished Bad Tag

Replace a tag only when no valid Release/assets exist and no server consumed it.

```
git ls-remote --tags origin refs/tags/v1.0.6
git push origin --delete v1.0.6
git tag -d v1.0.6

# Correct and commit first, then recreate:
git tag -a v1.0.6 -m "Project Forge WMS v1.0.6"
git push origin v1.0.6
```

**Published or installed tag:** never move it. Publish a new version and build.

## 8. Staging Installation and Testing

1. Confirm Plesk Git mode is manual; do not deploy the repository.
2. Open staging System Update and click Check for Updates.
3. Verify local/remote version and build, channel `<span class="editor-theme-code">staging</span>`, tag, requirements, and migrations.
4. For migrations, create and verify an external database backup.
5. Click Update exactly once.
6. The Plesk PHP scheduled task claims the queued job.
7. Wait for status `<span class="editor-theme-code">done</span>` and phase `<span class="editor-theme-code">complete</span>`.

`<span class="editor-theme-code">downloadverifyextractpreflightbackupinstallmigratepost-updatecleanupcomplete</span>`

```
APP_ROOT=/var/www/vhosts/staging.wms.thethreelogistics.com

grep -E '"version"|"build"' "$APP_ROOT/httpdocs/manifest.json"
ls -lt "$APP_ROOT/storage/system_updates/logs"
sed -n '1,300p' "$APP_ROOT/storage/system_updates/logs/JOB_ID.log"
ls -l "$APP_ROOT/config/system_update.php"
```

### Required staging tests

- Installed manifest exactly matches the candidate version/build.
- A new update check no longer offers the installed release.
- Login, permissions, navigation, layouts, and target modules render.
- Critical WMS workflows and background jobs succeed.
- Uploads, attachments, private config, and protected paths are unchanged.
- Job log confirms checksum verification and completion.

## 9. Record Staging Approval

<table id="bkmrk-tag-and-commitexact-"><colgroup><col></col><col></col></colgroup><tbody><tr><th>Tag and commit

</th><td>Exact tag and full commit SHA

</td></tr><tr><th>Artifact

</th><td>ZIP filename and SHA-256

</td></tr><tr><th>Job

</th><td>Staging job ID and completion time

</td></tr><tr><th>Database backup

</th><td>Backup ID/time when migrations exist

</td></tr><tr><th>Testing

</th><td>Tester, results, issues, and approval time

</td></tr></tbody></table>

## 10. Promote Without Rebuilding

1. Open the existing tested GitHub Release.
2. Edit it and clear **Set as a pre-release**.
3. Save without changing the tag or assets.

**Immutability gate:** tag, commit, ZIP bytes, filename, and SHA-256 must remain identical to staging.

## 11. Production Deployment

### 11.1 Readiness

- Production channel is `<span class="editor-theme-code">production</span>`.
- Updater foundation and private storage are already installed and tested.
- Plesk task is *Run a PHP script*, PHP 8.5, every minute.
- Plesk Git automatic deployment is disabled.
- Application and database backups are current and recoverable.
- Monitoring, maintenance window, and responsible operators are ready.

### 11.2 Install and verify

1. Click Check for Updates.
2. Confirm channel, stable tag, version, build, requirements, and checksum identity.
3. Confirm backups and click Update once.
4. Monitor until `<span class="editor-theme-code">done</span>`/`<span class="editor-theme-code">complete</span>`.
5. Verify manifest, log, protected paths, health, and business workflows.

```
APP_ROOT=/var/www/vhosts/production.example.com

grep -E '"version"|"build"' "$APP_ROOT/httpdocs/manifest.json"
ls -lt "$APP_ROOT/storage/system_updates/logs"
sed -n '1,300p' "$APP_ROOT/storage/system_updates/logs/JOB_ID.log"
```

## 12. Failure Rules

- Do not repeatedly click Update; inspect the existing job and its log.
- Do not move the tag or replace assets after consumption.
- Correct a failed candidate by publishing a new version/build.
- Before migration, verify automatic file rollback results.
- During/after migration, stop and use the external database recovery plan.
- Preserve logs, inventory, checksum, tag, and commit as incident evidence.

## 13. Final Checklist

1. Changes reviewed; syntax and tests pass.
2. Manifest version/build/date/notes are correct.
3. Release commit is on `<span class="editor-theme-code">origin/main</span>`.
4. Tag points to that release commit.
5. GitHub prerelease and both custom assets exist.
6. Staging installs and validates the exact artifact.
7. Release is promoted by metadata only.
8. Production readiness and backup gates pass.
9. Production installs and validates the same immutable artifact.

# API endpoints

# Implementing a New WMS API Endpoint

This guide explains the standard process for adding an endpoint to the existing WMS API. It is written for a developer who is new to this codebase.

## Purpose

The WMS API uses one central entry point. API requests are sent to the API index file. That file determines the request path and method, then sends the request to the correct endpoint handler. Shared response, authentication, permission, URL, and request-reading helpers are kept in the API bootstrap file.

Project Forge is in Implementation Mode. Preserve the current API structure, authentication, response format, and route style unless the request specifically requires an architectural change.

## Files Used for an Endpoint

<table id="bkmrk-filepurposehttpdocs%2F"><colgroup><col></col><col></col></colgroup><tbody><tr><th>File

</th><th>Purpose

</th></tr><tr><td>httpdocs/api/index.php

</td><td>Contains endpoint handlers, the route list, and request dispatching.

</td></tr><tr><td>httpdocs/api/bootstrap.php

</td><td>Contains common JSON response, authentication, permission, URL, and request helpers.

</td></tr><tr><td>API rewrite configuration file

</td><td>Forwards API requests to the central API index file. It normally does not change when adding a route.

</td></tr></tbody></table>

## Plan the Endpoint First

Before editing code, define the endpoint in operational terms.

- What WMS record or warehouse action does it support?
- Who may use it?
- Is it read-only, or does it change WMS data?
- What information is required from the caller?
- What result should be returned?
- How will access be limited to the correct client?

Use business-oriented route names that match existing routes. Orders, shipments, and inventory are examples. Review the existing API routes before selecting a new name.

## Required Security Rules

### Use the Existing Authentication Flow

The current WMS API uses the authenticated WMS session. It supports the WMS and the client portal. It is not currently designed as an external integration API with separate API keys or OAuth access.

Do not bypass the existing authentication or permission flow when adding an endpoint.

### Protect Client Data

A client user may only view or change records that belong to that client. This rule applies to every endpoint that reads or changes client data.

Use the authenticated user context to determine the permitted client. Do not trust a client identifier supplied in the request by itself. Enforce ownership in the record lookup or database query.

### Validate Input

Validate required path values, query values, and request fields before reading or changing data. Reject missing, malformed, or unsupported values through the existing API error response helper.

### Use the Standard Response Format

Every endpoint must use the existing API success and error response helpers. Do not create custom JSON formats for a single endpoint and do not print JSON directly from the handler.

## Implementation Procedure

### 1. Find a Similar Endpoint

Open the API index file and locate the closest existing endpoint. Follow its approach for permissions, client scoping, response fields, error handling, and route dispatching.

### 2. Add a Handler

Add the business-logic handler in the handler section of the API index file. Use a clear name that identifies the resource and action.

A handler should follow this order:

1. Read and validate the required input.
2. Confirm the authenticated user has permission for the action.
3. Determine the permitted client from the authenticated user.
4. Load the requested record with client ownership enforced when applicable.
5. Return a standard error if the record is missing or unavailable.
6. Perform the allowed read or write action.
7. Return the result through the standard success helper.

### 3. Register the Route

Add the route to the API route list in the API index file. This keeps the available routes discoverable and consistent with the current dispatcher.

### 4. Connect the Dispatcher

In the API route-dispatch section, add the path and request-method match that calls the new handler.

- Use a read request branch for a read-only endpoint.
- Use a create or action request branch for an endpoint that changes data.
- Keep related routes inside the existing parent resource section, such as orders or shipments.
- Use the existing method-not-allowed response when a route does not support the requested method.

### 5. Confirm Routing

A normal route addition should not require a change to the API rewrite file. If the new API URL returns a server-level not-found page instead of an API JSON response, verify that the rewrite rule still forwards the request to the API index file.

## Route Examples

<table id="bkmrk-purposeroute-shapere"><colgroup><col></col><col></col><col></col></colgroup><tbody><tr><th>Purpose

</th><th>Route Shape

</th><th>Request Method

</th></tr><tr><td>List records

</td><td>Orders collection

</td><td>GET

</td></tr><tr><td>Read one record

</td><td>Orders collection followed by an order identifier

</td><td>GET

</td></tr><tr><td>Perform a controlled action

</td><td>Order record followed by an action name

</td><td>POST

</td></tr></tbody></table>

These are route-shape examples only. Match the actual route conventions already used in the API index file.

## Testing Before Release

1. Run the PHP syntax check on each PHP file that changed.
2. Test the normal successful request while logged in as an authorized WMS user.
3. Test a request with missing required information.
4. Test an invalid or non-existent record identifier.
5. Test an unsupported request method.
6. Test with a client-scoped user and confirm that another client record cannot be viewed or changed.
7. For a write endpoint, confirm the WMS record changes once and the response accurately reflects the completed action.

Because the API uses the logged-in WMS session, use a logged-in browser session or an approved session-aware test process. Do not place credentials, session values, tokens, or client data in source code, screenshots, documentation, or Git commits.

## Endpoint Completion Checklist

- The endpoint purpose, route, method, permission, and response fields are defined.
- A handler was added using the existing WMS pattern.
- Input validation uses the standard API error response.
- Client ownership is enforced for client data.
- The route is listed and connected in the API index file.
- Success and error responses use the existing API helpers.
- PHP syntax checks pass.
- Success, validation, not-found, permission, and client-isolation tests pass.
- The endpoint is documented before release.

## Documentation Required for Every Endpoint

When a new endpoint is added, document the following in the WMS developer documentation:

- Endpoint name and purpose
- Request method and route
- Required authentication and permissions
- Client-scoping behavior
- Required path, query, and request values
- Success response fields
- Expected error responses
- Test procedure

Keeping this information current allows the next developer to extend the WMS safely without changing its established architecture.