# Ubuntu and Apache Deployment

This guide deploys DocuDeck with the React frontend served directly by Apache and the Laravel API served by Apache/PHP-FPM.

## Production addresses

- Frontend: `https://tools.abdalrhman.dev`
- Laravel API: `https://toolsapi.abdalrhman.dev`
- Project directory: `/var/www/html/docs_tools`

## Architecture

```text
Browser -> Apache -> React dist files
Browser -> Apache/PHP-FPM -> Laravel API -> MySQL
                                      -> database queue -> Supervisor worker
                                                           -> LibreOffice -> PDF
                                                           -> Poppler -> JPEG slides
```

The frontend is a static Vite build and does not require a Node.js service after it is built. Supervisor is required for reliable PowerPoint conversion because it keeps the Laravel queue worker running. The Laravel scheduler cron is not currently required.

## 1. Install system requirements

The project requires PHP 8.2 or newer and Node.js 22.13 or newer. The commands below use PHP 8.3 on Ubuntu 24.04.

```bash
sudo apt update
sudo apt install -y \
  apache2 mysql-server git unzip supervisor composer \
  php8.3 php8.3-fpm php8.3-cli php8.3-mysql \
  php8.3-curl php8.3-mbstring php8.3-xml php8.3-zip \
  libreoffice poppler-utils ffmpeg \
  fonts-dejavu fonts-liberation fonts-noto-core fontconfig python3-venv locales
```

Install Node.js 22.13 or newer using the server's preferred Node.js installation method.

Enable services and Apache modules:

```bash
sudo a2enmod rewrite proxy_fcgi setenvif headers ssl
sudo a2enconf php8.3-fpm
sudo systemctl enable --now php8.3-fpm apache2 mysql supervisor
```

Verify the installations:

```bash
php -v
node -v
composer --version
soffice --version
pdftoppm -v
ffmpeg -version
ffprobe -version
command -v php
command -v soffice
command -v pdftoppm
```

Expected executable paths are normally `/usr/bin/php`, `/usr/bin/soffice`, and `/usr/bin/pdftoppm`.

## 2. Download the project

```bash
cd /var/www/html
sudo git clone git@github.com:Abdalrhmankhashashneh/docs_tools.git
sudo chown -R "$USER":www-data /var/www/html/docs_tools
cd /var/www/html/docs_tools
```

The server must have an SSH key authorized to access the GitHub repository.

## 3. Create the MySQL database

```bash
sudo mysql
```

```sql
CREATE DATABASE documents_generator_tools
  CHARACTER SET utf8mb4
  COLLATE utf8mb4_unicode_ci;

CREATE USER 'documents_tools'@'localhost'
  IDENTIFIED BY 'REPLACE_WITH_A_STRONG_PASSWORD';

GRANT ALL PRIVILEGES
  ON documents_generator_tools.*
  TO 'documents_tools'@'localhost';

FLUSH PRIVILEGES;
EXIT;
```

## 4. Configure Laravel

```bash
cd /var/www/html/docs_tools/documents_generator_tools_backend
composer install --no-dev --optimize-autoloader
cp .env.example .env
php artisan key:generate
nano .env
```

Use production values:

```dotenv
APP_NAME="DocuDeck"
APP_ENV=production
APP_DEBUG=false
APP_URL=https://toolsapi.abdalrhman.dev

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=documents_generator_tools
DB_USERNAME=documents_tools
DB_PASSWORD=REPLACE_WITH_A_STRONG_PASSWORD

QUEUE_CONNECTION=database
DB_QUEUE_RETRY_AFTER=1900

FRONTEND_URL=https://tools.abdalrhman.dev
SANCTUM_STATEFUL_DOMAINS=tools.abdalrhman.dev
SESSION_DRIVER=database
SESSION_DOMAIN=.abdalrhman.dev
SESSION_SECURE_COOKIE=true
SESSION_SAME_SITE=lax

MAIL_MAILER=smtp
MAIL_HOST=REPLACE_WITH_SMTP_HOST
MAIL_PORT=587
MAIL_USERNAME=REPLACE_WITH_SMTP_USERNAME
MAIL_PASSWORD=REPLACE_WITH_SMTP_PASSWORD
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=no-reply@abdalrhman.dev
MAIL_FROM_NAME="DocuDeck"

CONVERSION_DISK=local
LIBREOFFICE_BINARY=/usr/bin/soffice
PDFTOPPM_BINARY=/usr/bin/pdftoppm
FFMPEG_BINARY=/usr/bin/ffmpeg
FFPROBE_BINARY=/usr/bin/ffprobe
MEDIA_PROCESS_TIMEOUT_SECONDS=1800
SIGNED_MEDIA_MINUTES=240
MEDIA_MAX_RESOURCE_KB=51200
FONT_PYTHON_BINARY=/var/www/html/docs_tools/documents_generator_tools_backend/.renderer-venv/bin/python
LAYOUT_PREVIEW_SIZE=1280
LAYOUT_PREVIEW_QUALITY=82
```

Do not add trailing slashes to `APP_URL` or `FRONTEND_URL`. Never commit the production `.env` file.

Use plain URLs, not Markdown links, in `.env`. Keep the existing `APP_KEY` when updating an existing installation; key generation and copying `.env.example` above are for a fresh installation only. Session authentication needs the real frontend origin and credential-enabled CORS; do not replace its allowed origin with `*`.

Provision the isolated renderer from the backend directory before enabling rendering:

```bash
sudo locale-gen en_US.UTF-8 ar_SA.UTF-8
python3 -m venv .renderer-venv
.renderer-venv/bin/pip install -r resources/font-renderer/requirements.txt
sudo -u www-data .renderer-venv/bin/python resources/font-renderer/renderer.py environment
```

The environment response must report `available: true`. Ubuntu uses this helper directly; Docker is required for the Windows workflow, not this Ubuntu deployment. Deploy all helper files together. Uploaded fonts remain private and are never installed globally.

Prepare Laravel:

```bash
php artisan migrate --force
php artisan optimize

sudo chown -R www-data:www-data storage bootstrap/cache
sudo chmod -R 775 storage bootstrap/cache
```

These permissions assume only trusted application/deployment accounts belong to the owning group. Apache must never expose `storage/app/private`, font files, working PowerPoints or preview snapshots. Back up the database and private storage before running additive migrations; never use `migrate:fresh`, database resets or generate a replacement application key during an update.

## 5. Configure PHP upload limits

Edit the PHP-FPM configuration:

```bash
sudo nano /etc/php/8.3/fpm/php.ini
```

Set:

```ini
upload_max_filesize = 210M
post_max_size = 220M
memory_limit = 512M
max_execution_time = 120
```

Restart PHP-FPM:

```bash
sudo systemctl restart php8.3-fpm
```

The 210 MB PHP limit covers both the 200 MB video limit and the 50 MB downloadable-resource limit. Resources accept PDF, DOC/DOCX, XLS/XLSX, PPT/PPTX, TXT, and ZIP. They are validated and stored under private conversion storage, then copied unchanged into generated SCORM packages. Include their size in `storage/app/private` backups, LMS package-limit checks, and deployment disk monitoring.

## 6. Configure the conversion worker

Create the Supervisor configuration:

```bash
sudo nano /etc/supervisor/conf.d/docudeck-worker.conf
```

```ini
[program:docudeck-worker]
process_name=%(program_name)s
directory=/var/www/html/docs_tools/documents_generator_tools_backend
command=/usr/bin/php artisan queue:work --queue=conversions --sleep=3 --tries=2 --timeout=1000 --max-time=3600
user=www-data
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
stopwaitsecs=1100
redirect_stderr=true
stdout_logfile=/var/www/html/docs_tools/documents_generator_tools_backend/storage/logs/worker.log
environment=HOME="/tmp"
```

Create a separate media worker at `/etc/supervisor/conf.d/docudeck-media-worker.conf`:

```ini
[program:docudeck-media-worker]
process_name=%(program_name)s
directory=/var/www/html/docs_tools/documents_generator_tools_backend
command=/usr/bin/php artisan queue:work --queue=media --sleep=3 --tries=2 --timeout=1800 --max-time=3600
user=www-data
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
stopwaitsecs=1900
redirect_stderr=true
stdout_logfile=/var/www/html/docs_tools/documents_generator_tools_backend/storage/logs/media-worker.log
environment=HOME="/tmp"
```

The media worker processes audio and video only. Downloadable resources become ready immediately after validation and do not require an additional queue or Supervisor process.

Create the layout-preview worker at `/etc/supervisor/conf.d/docudeck-layout-preview-worker.conf`:

```ini
[program:docudeck-layout-preview-worker]
process_name=%(program_name)s
directory=/var/www/html/docs_tools/documents_generator_tools_backend
command=/usr/bin/php artisan queue:work layout-previews --queue=layout-previews --sleep=3 --tries=1 --timeout=2100 --max-time=3600
user=www-data
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
stopwaitsecs=2200
redirect_stderr=true
stdout_logfile=/var/www/html/docs_tools/documents_generator_tools_backend/storage/logs/layout-preview-worker.log
environment=HOME="/tmp"
```

The positional `layout-previews` selects the dedicated database connection with a 2,200-second reservation. Do not run previews on the default connection or combine them with conversions/media. `DB_QUEUE_RETRY_AFTER=1900` keeps the shared database reservation longer than the media worker's 1,800-second timeout; it also covers the conversion worker's 1,000-second timeout. Adjust reservations if you increase timeouts.

Load and start all three workers:

```bash
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start docudeck-worker
sudo supervisorctl start docudeck-media-worker
sudo supervisorctl start docudeck-layout-preview-worker
sudo supervisorctl status
```

The expected status is `RUNNING`.

## 7. Build the frontend

```bash
cd /var/www/html/docs_tools/documents_generator_tools_frontend
cp .env.example .env.production
nano .env.production
```

Set the public API address:

```dotenv
VITE_API_URL=https://toolsapi.abdalrhman.dev/api/v1
```

Build the static application:

```bash
npm ci
npm run build
```

Vite embeds `VITE_API_URL` during the build. Rebuild whenever this value changes. The generated `dist` directory includes `.htaccess`, `index.html`, assets, and public files.

## 8. Configure the frontend virtual host

```bash
sudo nano /etc/apache2/sites-available/docudeck-frontend.conf
```

```apache
<VirtualHost *:80>
    ServerName tools.abdalrhman.dev
    DocumentRoot /var/www/html/docs_tools/documents_generator_tools_frontend/dist

    <Directory /var/www/html/docs_tools/documents_generator_tools_frontend/dist>
        Options -Indexes +FollowSymLinks
        AllowOverride All
        Require all granted
    </Directory>

    ErrorLog ${APACHE_LOG_DIR}/docudeck-frontend-error.log
    CustomLog ${APACHE_LOG_DIR}/docudeck-frontend-access.log combined
</VirtualHost>
```

`AllowOverride All` permits `dist/.htaccess` to route direct frontend URLs such as `/tools/powerpoint-to-scorm` to `index.html`.

## 9. Configure the API virtual host

```bash
sudo nano /etc/apache2/sites-available/docudeck-api.conf
```

```apache
<VirtualHost *:80>
    ServerName toolsapi.abdalrhman.dev
    DocumentRoot /var/www/html/docs_tools/documents_generator_tools_backend/public

    <Directory /var/www/html/docs_tools/documents_generator_tools_backend/public>
        Options -Indexes +FollowSymLinks
        AllowOverride All
        Require all granted
    </Directory>

    LimitRequestBody 230686720

    ErrorLog ${APACHE_LOG_DIR}/docudeck-api-error.log
    CustomLog ${APACHE_LOG_DIR}/docudeck-api-access.log combined
</VirtualHost>
```

Apache must expose only Laravel's `public` directory, never the backend project root.

Apply the request-body limit to the HTTPS virtual host too. This 220 MiB limit accommodates 200 MB video uploads plus multipart overhead; the previous 120 MB limit blocked larger supported videos before Laravel validation.

Enable both sites:

```bash
sudo a2ensite docudeck-frontend.conf docudeck-api.conf
sudo apachectl configtest
sudo systemctl reload apache2
```

## 10. Configure DNS and HTTPS

Point these DNS records to the Ubuntu server:

```text
tools.abdalrhman.dev     -> server IP
toolsapi.abdalrhman.dev  -> server IP
```

Install Certbot and issue certificates:

```bash
sudo apt install -y certbot python3-certbot-apache
sudo certbot --apache -d tools.abdalrhman.dev -d toolsapi.abdalrhman.dev
```

## 11. Verify the deployment

Check the frontend and direct SPA route:

```bash
curl -I https://tools.abdalrhman.dev
curl -I https://tools.abdalrhman.dev/tools/powerpoint-to-scorm
```

Check Laravel:

```bash
curl -I https://toolsapi.abdalrhman.dev/up
```

Check CORS:

```bash
curl -i -X OPTIONS \
  https://toolsapi.abdalrhman.dev/api/v1/powerpoint-to-scorm/conversions \
  -H "Origin: https://tools.abdalrhman.dev" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: content-type"
```

The response must include:

```text
Access-Control-Allow-Origin: https://tools.abdalrhman.dev
```

Check the worker and logs:

```bash
sudo supervisorctl status docudeck-worker
tail -f /var/www/html/docs_tools/documents_generator_tools_backend/storage/logs/worker.log
tail -f /var/www/html/docs_tools/documents_generator_tools_backend/storage/logs/laravel.log
```

Finally, sign in and upload a small valid `.pptx`. Confirm it opens in **Preparation**, upload permitted fonts, choose rendering settings and explicitly generate images. Open **Fix slide layout** and verify the initial preview renders all thumbnails progressively. Edit one box, wait for **Saved**, and confirm that slide appears under **Needs preview**. Click **Preview changes** and verify LibreOffice exports only the affected page, only that slide is rasterized, and unchanged cached thumbnails remain available. Request **Original** to verify lazy comparison. Manual review is no longer required. Substitution acknowledgement is required for final image generation, not temporary layout previews. Then generate/download a SCORM ZIP.

## 12. Deploy future updates

Back up MySQL and `storage/app/private`, check the worktree and verify the selected release before updating. Do not overwrite the production `.env`. Schedule a maintenance window for helper updates and allow running render jobs to finish; mixed helper versions can invalidate in-flight work.

### Preflight and verified backup

Run the update from one shell so the release and backup identifiers remain available. Replace `docudeck_database` and `docudeck_database_user` with the production database values. The database password is requested interactively; do not place it in shell history.

```bash
set -euo pipefail

cd /var/www/html/docs_tools
release_stamp=$(date -u +%Y%m%dT%H%M%SZ)
previous_release=$(git rev-parse HEAD)
backup_dir="/var/backups/docudeck/${release_stamp}"

git status --short
git fetch origin --prune
git log --oneline HEAD..origin/main
git merge-base --is-ancestor HEAD origin/main

sudo install -d -m 0700 "$backup_dir"
printf '%s\n' "$previous_release" | sudo tee "$backup_dir/previous-release.txt" >/dev/null
sudo cp -a documents_generator_tools_backend/.env "$backup_dir/backend.env"
sudo tar -C documents_generator_tools_backend -czf "$backup_dir/private-storage.tar.gz" storage/app/private
mysqldump --single-transaction --routines --triggers --events -u docudeck_database_user -p docudeck_database | gzip | sudo tee "$backup_dir/database.sql.gz" >/dev/null

sudo test -s "$backup_dir/backend.env"
sudo tar -tzf "$backup_dir/private-storage.tar.gz" >/dev/null
sudo gzip -t "$backup_dir/database.sql.gz"
```

Stop if `git status --short` reports local changes, the remote history is not a fast-forward, a backup command fails, or an archive cannot be verified. Store production backups outside the Git checkout with root-only permissions and copy them to the configured backup system.

### Prepare the reviewed release

Use a fast-forward update instead of an implicit merge. Install dependencies and build a versioned frontend directory before entering maintenance mode. This keeps the currently published `dist` available if compilation fails.

```bash
cd /var/www/html/docs_tools
git merge --ff-only origin/main
deployed_release=$(git rev-parse HEAD)
git show --no-patch --oneline "$deployed_release"

cd documents_generator_tools_backend
composer install --no-dev --prefer-dist --no-interaction --optimize-autoloader
.renderer-venv/bin/pip install -r resources/font-renderer/requirements.txt
sudo -u www-data .renderer-venv/bin/python resources/font-renderer/renderer.py environment
php artisan migrate:status

cd ../documents_generator_tools_frontend
npm ci
frontend_release_dir="dist-${release_stamp}"
npm run build -- --outDir "$frontend_release_dir"
test -f "$frontend_release_dir/index.html"
```

### Migrate and publish

Put the Laravel API into maintenance mode only after dependencies and the new frontend have built successfully. Never run `migrate:fresh`, `db:wipe`, `key:generate`, or copy `.env.example` over `.env` during an update.

```bash
cd /var/www/html/docs_tools/documents_generator_tools_backend
php artisan down --retry=60
php artisan optimize:clear
php artisan migrate --force
php artisan optimize
php artisan queue:restart

cd ../documents_generator_tools_frontend
mv dist "dist-previous-${release_stamp}"
mv "$frontend_release_dir" dist

sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl status
sudo systemctl reload php8.3-fpm
sudo apachectl configtest
sudo systemctl reload apache2

cd ../documents_generator_tools_backend
php artisan up
```

Supervisor automatically restarts workers after `queue:restart` once their current jobs finish. Confirm all configured programs are `RUNNING`; for newly added/changed Supervisor configurations use `reread` and `update` as above. Hard-refresh the browser if it still shows the old frontend.

### Post-deployment verification

```bash
cd /var/www/html/docs_tools
test "$(git rev-parse HEAD)" = "$deployed_release"

cd documents_generator_tools_backend
php artisan migrate:status
php artisan queue:failed
sudo supervisorctl status
curl --fail --silent --show-error https://api.example.com/up
curl --fail --silent --show-error https://app.example.com/ >/dev/null
```

Then test an existing project before creating new data: sign in, open it, confirm its slides and settings, make and save one reversible change, generate a replacement package, and verify the previous package remains available until the replacement succeeds. Test any feature introduced by the release with a dedicated non-production project.

If verification fails, put the API back into maintenance mode and investigate before accepting traffic. Prefer disabling the affected feature flag or deploying a forward fix. Do not run migration rollback on production merely to match older code: additive tables can contain new user data. A database restore is a last resort and must restore the matching database and private-storage backups from the same `release_stamp`. Keep `dist-previous-${release_stamp}` until acceptance is complete, then remove it during a separate reviewed cleanup.

### Preview and review troubleshooting

- **Queued/loading indefinitely:** check `sudo supervisorctl status docudeck-layout-preview-worker` and `storage/logs/layout-preview-worker.log`. Verify the exact connection/queue command above, database connectivity and private storage permissions. The first requested slide still waits for LibreOffice to open the presentation and export the target-page PDF, but a one-slide correction must not create an all-slide PDF. Confirm both `/usr/bin/pdfinfo` and `/usr/bin/pdftoppm` are installed through `poppler-utils`. Do not clear the jobs table.
- **POST `/layout/previews` returns 422:** inspect the JSON validation message and verify that `priority_slide` and the revision values are valid. Font warnings are informational for temporary previews and no longer require acknowledgement.
- **Rendering failed:** inspect the preview worker and Laravel logs; run the environment check as `www-data`, confirm locales, helper dependencies and sufficient memory/disk. Failures must retain previous published images/packages. Do not force project status or revision fields in SQL.
- **Image failed to load:** use **Retry image**. Inspect the authenticated image request's response and the Laravel log; do not expose private storage publicly to work around authorization failures.
- **Needs preview remains visible:** wait for Saved, click **Preview changes**, and check the layout-preview worker. Only affected layout slides should be queued. Font or rendering-setting changes intentionally require all slides again.
- **POST `/render` returns 409:** read the JSON response message. A stale preparation revision requires reloading current preparation, an active job must finish, and a source-hash conflict needs investigation rather than bypassing it.
- **Archived projects:** restore before editing or rendering. Previously generated packages remain downloadable while archived.

Useful read-only checks, from the backend directory:

```bash
php artisan migrate:status
php artisan queue:failed
sudo supervisorctl status
tail -n 100 storage/logs/layout-preview-worker.log
tail -n 100 storage/logs/laravel.log
```

Review failed-job details before retrying a specific job. Layout preview jobs use one active snapshot per project and application-level claim checks; do not bulk-retry interrupted preview jobs blindly—request a fresh preview through the editor when necessary.

## Operations notes

- No Laravel scheduler cron is required for the current stage.
- Do not run `php artisan serve` in production; Apache and PHP-FPM serve the API.
- Do not run a Node.js frontend service; Apache serves `dist` directly.
- Presentation conversion requires both LibreOffice and Poppler.
- Uploaded sources and generated packages are retained indefinitely in Stage 1. Monitor disk usage and back up `storage/app/private`.
- Restart the worker after backend deployments or relevant `.env` changes.
## Stage 5 renderer prerequisites

Follow [Stage 5 local renderer setup](stage-05-local-setup.md) for the Ubuntu helper virtualenv, font dependencies and locale generation. Set `FONT_PYTHON_BINARY` to the absolute virtualenv Python path. New uploads create preparing projects; image generation starts explicitly after font and rendering settings review. Do not globally install uploaded fonts or expose the private conversion disk.

After deploying: run `php artisan migrate --force`, `php artisan optimize:clear` and `php artisan queue:restart`, then restart the supervised conversion worker. The `conversions` worker handles initial and regenerated image jobs. Keep the separate `media` worker running. Record the isolated renderer integration test separately from the legacy conversion smoke test before declaring Stage 5 accepted.
# Stage 6 layout previews

After deploying Stage 6, run the additive migrations and restart existing workers. Configure a separate supervised process:

```bash
php artisan queue:work layout-previews --queue=layout-previews --timeout=2100 --tries=1
```

Its database queue connection reserves jobs for 2,200 seconds; do not route this workload through the shorter conversions reservation. Keep the same private storage permissions and Linux font-renderer dependencies described for Stage 5. Preview jobs do not publish course images. See `docs/stage-06-pre-conversion-slide-layout-editor.md` for pending acceptance checks.

Stage 6 extraction contract is now v2. Deploy all helper Python files together; older cached inventories refresh without deleting valid corrections. `IsolatedSlidePreviewRenderer` uses the helper's selected-page `preview` operation; `IsolatedPresentationRenderer` keeps the complete `render` operation for Generate/Regenerate images. Preview PDF export uses LibreOffice `PageRange`, validates the result with `pdfinfo`, and falls back to full-document export for incompatible LibreOffice versions. Rebuild `docudeck-renderer:1` on Windows after helper changes. On Ubuntu use the configured virtualenv Python and restart both conversions and layout-preview workers after updating. Do not run database resets. Keep private source/font/snapshot/output directories writable only by the application/worker account.

Local Windows Docker repair, final rendering and automated regressions pass. Native Ubuntu and Moodle acceptance are still pending: run the real renderer integration/helper tests with permitted font fixtures, then inspect corrected Arabic output and verify SCORM resume, media, questions and scoring in Moodle before recording production acceptance. Mobile and full accessibility/interaction checks remain pending as documented in Stage 6.

# Stage 7 course composition

Stage 7 uses an additive migration that backfills stable course-slide records for existing projects. Back up the database and private project storage, deploy backend and frontend together, then run:

```bash
cd /var/www/html/docs_tools/documents_generator_tools_backend
php artisan migrate --force
php artisan optimize:clear
php artisan queue:restart
sudo supervisorctl restart docudeck-conversions:*
sudo supervisorctl restart docudeck-media:*
sudo supervisorctl restart docudeck-layout-previews:*
```

Rebuild and publish the frontend in the same release because Stage 7 API responses use stable course-slide IDs. No new queue is required. Do not run `migrate:fresh`; the migration preserves original PowerPoint slide files, questions, media, layout corrections, and existing packages. Existing packages remain downloadable and are labelled outdated after structural edits until a replacement package succeeds.

After deployment, verify one existing project and one new project: reorder a slide, hide another from the menu, exclude and restore a slide, duplicate a slide, create a question group, generate config v5, and resume the package in Moodle. Confirm hidden slides remain in sequential playback and excluded slides do not contribute to progress.

# Stage 8 project sharing and public links

Stage 8 sharing uses two additive access tables. It does not update `conversions.user_id`, existing revisions, project files, rendered slides, generated packages, or learner-state formats. H5P remains on its separate branch and is not included in this `main` deployment.

Before deploying, add these values to the existing production backend `.env` without replacing the file:

```dotenv
PROJECT_SHARING_ENABLED=false
PROJECT_SHARING_ASSET_MINUTES=5
```

Deploy the backend and frontend together using Section 12. Keep sharing disabled while running the additive migration and verifying an existing owner/project:

```bash
cd /var/www/html/docs_tools/documents_generator_tools_backend
php artisan migrate --force
php artisan optimize:clear
php artisan queue:restart

php artisan migrate:status
php artisan test --filter=ProjectSharingTest
```

No additional queue or scheduler is required. Confirm the sharing migration created `conversion_shares` and `conversion_share_links`. Then sign in as an existing owner and verify an old project still opens, saves, packages, and downloads before enabling the feature.

Enable sharing only after that smoke test:

```bash
sudo sed -i 's/^PROJECT_SHARING_ENABLED=.*/PROJECT_SHARING_ENABLED=true/' /var/www/html/docs_tools/documents_generator_tools_backend/.env
cd /var/www/html/docs_tools/documents_generator_tools_backend
php artisan optimize:clear
sudo systemctl reload php8.3-fpm
```

Test with separate browser profiles:

1. Owner shares a test project with one existing viewer and one existing editor.
2. Viewer opens the learner preview but cannot rename, save, upload, render, package, archive, delete, or manage shares.
3. Editor can save normal authoring changes and generate/download a package, but cannot manage sharing or project lifecycle actions.
4. Owner creates a public link, copies it while the raw secret is visible, and opens it in a signed-out private window.
5. Rotate the link and confirm the old URL fails immediately; disable the new link and confirm it returns unavailable.
6. Confirm public responses do not expose the original PowerPoint name, removed questions, internal errors, or private paths. Signed public assets expire after five minutes.
7. Disable package download and confirm viewer/public download requests receive `403`; enable it and verify the already-generated ZIP downloads.

If an issue appears, set `PROJECT_SHARING_ENABLED=false`, clear the configuration cache, and reload PHP-FPM. This restores owner-only behavior while preserving the additive share rows for investigation. Do not run `migrate:rollback` after users have created shares or links, and never use `migrate:fresh`, `db:wipe`, or table truncation. Code rollback must tolerate the two unused additive tables.

## Stage 8 personal project folders

Project folders are also additive. The migration creates `project_folders` and `project_folder_assignments`; it does not add or rewrite a column on `conversions`. Consequently, every existing project appears under **Unfiled** immediately after deployment. Folder assignments belong to each user, so collaborators can organize a shared project without changing the owner's folders.

Record the project count before the migration, apply it, and verify that the count is unchanged:

```bash
cd /var/www/html/docs_tools/documents_generator_tools_backend

php artisan tinker --execute='echo "conversions_before=" . \App\Models\Conversion::count() . PHP_EOL;'
php artisan migrate --force
php artisan tinker --execute='echo "conversions_after=" . \App\Models\Conversion::count() . "; folders=" . \App\Models\ProjectFolder::count() . "; assignments=" . \App\Models\ProjectFolderAssignment::count() . PHP_EOL;'
php artisan test --filter=ProjectFolderTest
```

On the first deployment, `folders=0` and `assignments=0` are expected while `conversions_before` and `conversions_after` must match exactly. Existing project files, slides, questions, media, packages, sharing permissions, and learner data are not touched.

After publishing the rebuilt frontend, verify:

1. An existing project is visible in **All projects** and **Unfiled** and still opens normally.
2. Create a folder, move the existing project through its menu, and move it back to **Unfiled**.
3. Drag a disposable test project onto a folder and confirm the project remains editable.
4. Delete that folder and confirm its projects return to **Unfiled**; no project or private file is deleted.
5. As a collaborator, place a shared project in a personal folder and confirm the owner's organization is unchanged.
6. Check desktop, mobile, English, and Arabic/RTL dashboard layouts.

The folder foreign key cascades only into `project_folder_assignments`; there is no cascade path from a folder to `conversions`. Do not use `migrate:rollback` after users create folders. If the interface must be rolled back, deploy the previous frontend/backend while leaving these additive tables in place until a reviewed cleanup release.
