diff --git a/.env.example b/.env.example index 87928d4..952719b 100644 --- a/.env.example +++ b/.env.example @@ -41,6 +41,8 @@ TSE_ATTACHMENT_DISK=public TSE_ATTACHMENT_ROOT=attachments TSE_ATTACHMENT_MAX_KB=10240 TSE_ATTACHMENT_MIME_TYPES=image/jpeg,image/png,image/webp,application/pdf,text/plain,text/csv,application/zip +TSE_BACKUP_DIR= +TSE_BACKUP_STORAGE=1 CACHE_STORE=database # CACHE_PREFIX= diff --git a/CHANGELOG.md b/CHANGELOG.md index e370017..36c79f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ This project uses Git as the primary rollback trail. The `.old` convention is re - Added post-work provider tag verification so buyers can confirm/dispute observed technician level and declared provider tags after completed work orders, with buyer-endorsed tag evidence shown on provider profiles. - Added Phase 11A mobile-safe API actions with versioned work-order endpoints, token ability checks, participant authorization, checklist/status/message/evidence/contact/running-late/schedule/dispute actions, mobile event audit records, optional geolocation evidence, API throttling, and baseline security headers. - Added Phase 12A/13A notification and admin governance controls with notification preference UI, event-level notification gating, moderation reports for profiles/jobs/attachments, reusable audit logs, and expanded admin triage panels. +- Added Phase 14A deployment/scaling hardening with high-use database indexes, backup and health-check scripts, optional pre-update backups, and production notes for queue workers, scheduler, PHP-FPM, Redis, storage scaling, log rotation, and database scaling. - Created the Laravel Provider Exchange prototype with Breeze authentication, seeded demo users, and role support for admins, providers, buyers, and hybrid accounts. - Added provider and buyer profile management with service areas, profile details, and external profile snapshot records. - Added social posts, buyer job posts, provider quotes, quote revisions, quote decline flow, and buyer quote acceptance. @@ -45,6 +46,7 @@ This project uses Git as the primary rollback trail. The `.old` convention is re - `php artisan test` passed under Windows PHP with 51 tests and 256 assertions after post-work provider tag verification. - `php artisan test` passed under Windows PHP with 55 tests and 285 assertions after Phase 11A mobile API and security hardening. - `php artisan test` passed under Windows PHP with 57 tests and 301 assertions after Phase 12A/13A notification preferences and admin governance. +- `php artisan migrate:fresh --seed --env=testing`, `php artisan test` with 57 tests and 301 assertions, `npm run build`, and `bash scripts/health-check.sh` passed after Phase 14A deployment/scaling hardening. - `npm run build` passed with a production Vite build. - `https://christit.com/tse/login` loads publicly. - Seeded admin login succeeds on `https://christit.com/tse` and reaches `/tse/dashboard`. diff --git a/database/migrations/2026_07_11_002000_add_scaling_indexes.php b/database/migrations/2026_07_11_002000_add_scaling_indexes.php new file mode 100644 index 0000000..d9cea3c --- /dev/null +++ b/database/migrations/2026_07_11_002000_add_scaling_indexes.php @@ -0,0 +1,140 @@ +index(['visibility', 'status', 'created_at'], 'idx_job_posts_visibility_status_created'); + $table->index(['status', 'work_category_id', 'required_technician_level'], 'idx_job_posts_board_category_level'); + $table->index(['scope_clarity_status', 'contact_certified', 'remote_eligible'], 'idx_job_posts_safety_filters'); + $table->index(['buyer_id', 'status', 'created_at'], 'idx_job_posts_buyer_status_created'); + $table->index('starts_at', 'idx_job_posts_starts_at'); + }); + + Schema::table('provider_profiles', function (Blueprint $table) { + $table->index(['max_technician_level', 'public_contact'], 'idx_provider_profiles_level_public'); + $table->index('service_area', 'idx_provider_profiles_service_area'); + $table->index('insurance_status', 'idx_provider_profiles_insurance'); + }); + + Schema::table('buyer_profiles', function (Blueprint $table) { + $table->index('public_contact', 'idx_buyer_profiles_public_contact'); + }); + + Schema::table('quotes', function (Blueprint $table) { + $table->index(['job_post_id', 'status', 'created_at'], 'idx_quotes_job_status_created'); + $table->index(['provider_id', 'status', 'created_at'], 'idx_quotes_provider_status_created'); + }); + + Schema::table('work_orders', function (Blueprint $table) { + $table->index(['buyer_id', 'status', 'created_at'], 'idx_work_orders_buyer_status_created'); + $table->index(['provider_id', 'status', 'created_at'], 'idx_work_orders_provider_status_created'); + $table->index(['job_post_id', 'status'], 'idx_work_orders_job_status'); + $table->index('scheduled_at', 'idx_work_orders_scheduled_at'); + }); + + Schema::table('work_order_messages', function (Blueprint $table) { + $table->index(['work_order_id', 'created_at'], 'idx_work_order_messages_order_created'); + }); + + Schema::table('reviews', function (Blueprint $table) { + $table->index(['moderation_status', 'reported_at'], 'idx_reviews_moderation_reported'); + $table->index(['reviewee_id', 'created_at'], 'idx_reviews_reviewee_created'); + $table->index(['work_order_id', 'review_type'], 'idx_reviews_order_type'); + }); + + Schema::table('ratings', function (Blueprint $table) { + $table->index(['rateable_type', 'rateable_id', 'category', 'stars'], 'idx_ratings_target_category_stars'); + }); + + Schema::table('disputes', function (Blueprint $table) { + $table->index(['status', 'reason_code', 'created_at'], 'idx_disputes_status_reason_created'); + $table->index(['work_order_id', 'status'], 'idx_disputes_order_status'); + }); + + Schema::table('attachments', function (Blueprint $table) { + $table->index(['user_id', 'kind', 'created_at'], 'idx_attachments_user_kind_created'); + $table->index(['attachable_type', 'attachable_id', 'kind'], 'idx_attachments_target_kind'); + }); + + Schema::table('external_profile_imports', function (Blueprint $table) { + $table->index(['verification_status', 'created_at'], 'idx_external_imports_verification_created'); + $table->index(['provider_profile_id', 'visibility'], 'idx_external_imports_profile_visibility'); + }); + + Schema::table('notifications', function (Blueprint $table) { + $table->index(['notifiable_type', 'notifiable_id', 'read_at', 'created_at'], 'idx_notifications_inbox'); + }); + } + + public function down(): void + { + Schema::table('notifications', function (Blueprint $table) { + $table->dropIndex('idx_notifications_inbox'); + }); + + Schema::table('external_profile_imports', function (Blueprint $table) { + $table->dropIndex('idx_external_imports_verification_created'); + $table->dropIndex('idx_external_imports_profile_visibility'); + }); + + Schema::table('attachments', function (Blueprint $table) { + $table->dropIndex('idx_attachments_user_kind_created'); + $table->dropIndex('idx_attachments_target_kind'); + }); + + Schema::table('disputes', function (Blueprint $table) { + $table->dropIndex('idx_disputes_status_reason_created'); + $table->dropIndex('idx_disputes_order_status'); + }); + + Schema::table('ratings', function (Blueprint $table) { + $table->dropIndex('idx_ratings_target_category_stars'); + }); + + Schema::table('reviews', function (Blueprint $table) { + $table->dropIndex('idx_reviews_moderation_reported'); + $table->dropIndex('idx_reviews_reviewee_created'); + $table->dropIndex('idx_reviews_order_type'); + }); + + Schema::table('work_order_messages', function (Blueprint $table) { + $table->dropIndex('idx_work_order_messages_order_created'); + }); + + Schema::table('work_orders', function (Blueprint $table) { + $table->dropIndex('idx_work_orders_buyer_status_created'); + $table->dropIndex('idx_work_orders_provider_status_created'); + $table->dropIndex('idx_work_orders_job_status'); + $table->dropIndex('idx_work_orders_scheduled_at'); + }); + + Schema::table('quotes', function (Blueprint $table) { + $table->dropIndex('idx_quotes_job_status_created'); + $table->dropIndex('idx_quotes_provider_status_created'); + }); + + Schema::table('buyer_profiles', function (Blueprint $table) { + $table->dropIndex('idx_buyer_profiles_public_contact'); + }); + + Schema::table('provider_profiles', function (Blueprint $table) { + $table->dropIndex('idx_provider_profiles_level_public'); + $table->dropIndex('idx_provider_profiles_service_area'); + $table->dropIndex('idx_provider_profiles_insurance'); + }); + + Schema::table('job_posts', function (Blueprint $table) { + $table->dropIndex('idx_job_posts_visibility_status_created'); + $table->dropIndex('idx_job_posts_board_category_level'); + $table->dropIndex('idx_job_posts_safety_filters'); + $table->dropIndex('idx_job_posts_buyer_status_created'); + $table->dropIndex('idx_job_posts_starts_at'); + }); + } +}; diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index 2fce660..a12a89c 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -35,6 +35,10 @@ This document records the explicit runtime requirements and server changes for P - `TSE_ATTACHMENT_ROOT`, default `attachments`. - `TSE_ATTACHMENT_MAX_KB`, default `10240`. - `TSE_ATTACHMENT_MIME_TYPES`, comma-separated allowlist. +- Backup policy environment variables: + - `TSE_BACKUP_DIR`, optional backup destination override. + - `TSE_BACKUP_STORAGE`, default `1`, controls whether `storage/app` is archived with the database. + - `TSE_BACKUP_BEFORE_UPDATE`, default `0`, runs `scripts/backup.sh` before `scripts/update.sh` when set to `1`. ## Local Test Requirements @@ -53,6 +57,147 @@ This document records the explicit runtime requirements and server changes for P - Cron entry for `php artisan schedule:run`. - Shared/object storage before multi-server deployment. +## Queue Worker + +The current ChristIT test deployment uses the database queue driver. That is acceptable for low-volume testing, but real notification, email, import, and image-processing work should run under a persistent worker. + +Example systemd unit: + +```ini +[Unit] +Description=Provider Exchange queue worker +After=network.target + +[Service] +User=www-data +Group=www-data +Restart=always +WorkingDirectory=/sites/provider-exchange +ExecStart=/usr/bin/php artisan queue:work --sleep=3 --tries=3 --max-time=3600 + +[Install] +WantedBy=multi-user.target +``` + +Reload workers after deployment with: + +```bash +php artisan queue:restart +``` + +## Scheduler + +Laravel's scheduler should be invoked once per minute by cron: + +```cron +* * * * * cd /sites/provider-exchange && /usr/bin/php artisan schedule:run >> /dev/null 2>&1 +``` + +## PHP-FPM + +Keep the app behind a dedicated PHP-FPM pool when production traffic increases. The pool should have: + +- Its own Unix socket. +- A deploy user/group that can read the app and write `storage/` and `bootstrap/cache/`. +- `pm.max_children`, `pm.start_servers`, and spare server counts sized from observed memory usage. +- `request_terminate_timeout` high enough for uploads, but not high enough to pin workers indefinitely. +- `upload_max_filesize` and `post_max_size` aligned with `TSE_ATTACHMENT_MAX_KB`. + +Do not change unrelated PHP-FPM sockets used by existing ChristIT sites when tuning this app. + +## Redis Migration Plan + +Before running multiple web nodes or high-volume alerting, move transient state out of the database: + +```env +CACHE_STORE=redis +SESSION_DRIVER=redis +QUEUE_CONNECTION=redis +REDIS_CLIENT=phpredis +REDIS_HOST=127.0.0.1 +REDIS_PORT=6379 +``` + +After changing these values, run: + +```bash +php artisan optimize:clear +php artisan queue:restart +``` + +Database-backed notifications can stay in the database because they are user-visible records. Queue transport, sessions, and cache should move first. + +## Backups + +Run an on-demand backup from the app directory: + +```bash +bash scripts/backup.sh +``` + +The script supports SQLite, MySQL/MariaDB, and PostgreSQL based on `.env`. It writes a manifest beside the database backup. By default it also archives `storage/app`. + +Run an update with a preflight backup: + +```bash +TSE_BACKUP_BEFORE_UPDATE=1 bash scripts/update.sh +``` + +Recommended production posture: + +- Store backups outside the app directory with `TSE_BACKUP_DIR`. +- Replicate backups off-server. +- Test restore steps before relying on the backup plan. +- Keep evidence attachments and profile uploads under a retention policy that matches the platform's dispute and audit needs. + +## Health Check + +Run: + +```bash +bash scripts/health-check.sh +``` + +The check verifies Laravel boot, database/migration reachability, API and notification routes, writable cache/storage directories, storage symlink, and built Vite assets. + +## Log Rotation + +Laravel writes logs under `storage/logs`. Add a logrotate rule before real traffic: + +```conf +/sites/provider-exchange/storage/logs/*.log { + daily + missingok + rotate 14 + compress + delaycompress + notifempty + create 0640 www-data www-data +} +``` + +## Storage Scaling + +Local public storage is acceptable for the first ChristIT test deployment. Before multi-server deployment: + +- Move uploads/evidence to shared or object storage. +- Keep private evidence behind controller authorization instead of direct public URLs. +- Add malware scanning for uploaded evidence. +- Add thumbnail generation for large images. +- Add quota policy by user and work order. +- Keep attachment disk/path metadata stable so old evidence records continue to resolve after storage migration. + +## Database Scaling + +Phase 14A added indexes for high-use filters and timelines: jobs, profiles, quotes, work orders, messages, reviews, ratings, disputes, attachments, external imports, and notifications. + +Next database reliability steps after real usage begins: + +- Add cached reputation aggregates based on measured slow pages. +- Add query monitoring. +- Move from SQLite to MariaDB/MySQL or PostgreSQL before treating data as production-critical. +- Keep migrations additive where possible and back up before every schema-changing deployment. + ## ChristIT Test Deployment Target URL: diff --git a/docs/FORWARD_PHASE_PLAN.md b/docs/FORWARD_PHASE_PLAN.md index b03e134..f54ec68 100644 --- a/docs/FORWARD_PHASE_PLAN.md +++ b/docs/FORWARD_PHASE_PLAN.md @@ -30,17 +30,17 @@ The goal is not to clone Field Nation. The goal is to keep the useful operationa ### Current Weak Spots In Code - Job posting now has structured scope and support certification fields, but the creation UI should be refined into reusable templates and stronger validation before real marketplace use. -- Provider and buyer directories use text-based filters instead of normalized category, skill, coverage, remote, rating, and reliability filters. -- Work-order lists are card-based and lack the dense assigned/available-work table that worked well in Field Nation. -- Work-order detail now supports contact/support failure logging, first-class change requests, and post-work provider tag verification; schedule update requests, running-late records, and general report-problem records still need dedicated workflows. +- Provider and buyer directories have better filters than the prototype baseline, but still need saved searches, stronger availability signals, and deeper reliability filters. +- Work-order lists still need a UX pass for dense assigned-work tables and mobile-safe responsive alternatives. +- Work-order detail now supports contact/support failure logging, first-class change requests, post-work provider tag verification, running-late records, and schedule-update requests; the remaining need is a cleaner general report-problem workflow. - Disputes and votes now have reason codes, but quorum rules, visibility rules, and deeper evidence timelines still need a future pass. -- Reviews exist, but there is no review response, report flow, edit window, moderation queue, category definitions page, or imported/native reputation distinction in the UI. -- Universal ratings are flexible, but category names are arbitrary strings and not governed by a taxonomy. -- External profile imports store summary numbers and notes, but not structured work-category history, imported endorsement categories, imported review snapshots with privacy controls, or imported-vs-native display rules. +- Reviews now have category definitions, reviewee responses, report flow, edit windows, moderation queues, and imported/native reputation separation; the remaining reputation work is mostly aggregate performance and UI polish. +- Universal ratings are flexible, but category names still need stronger governance where they are not backed by explicit review categories. +- External profile imports now support structured work-category history, imported endorsements, imported review excerpts, visibility controls, proof attachments, and verification status; the remaining risk is better privacy copy and verification workflow polish. - Profile services, tools, certifications, hiring policies, and locations are JSON arrays entered through newline text boxes. That is acceptable for prototype speed, but not durable enough for serious filtering. -- Notification preferences exist as a model, but the UI does not expose channel and event preferences yet. -- API endpoints are read-heavy starters and do not yet map to mobile-safe work-order actions. -- Admin is only a status overview, not a moderation or operations console. +- Notification preferences now expose channel intent, categories, event gates, digest, and quiet hours, but email/push delivery providers are intentionally not enabled yet. +- API endpoints now cover mobile-safe work-order actions, but native app payloads should be refined once real app requirements are clearer. +- Admin now includes moderation reports and audit logs, but still needs deeper operational filters, safety actions, and queue/system health panels. ## Revised Phases From Here @@ -715,6 +715,16 @@ Provider-protection rules: - Private attachments must stay private. - Update/deployment failures should not corrupt active work orders. +Implementation status: + +- Completed first implementation pass. +- Added high-use database indexes for job filters, profile filters, quotes, work-order status/timelines, messages, ratings, reviews, disputes, attachments, imported-history queues, and notification inbox lookups. +- Added `scripts/backup.sh` for SQLite, MySQL/MariaDB, and PostgreSQL backups with optional `storage/app` archive support. +- Added `scripts/health-check.sh` for deployment smoke checks. +- Added optional `TSE_BACKUP_BEFORE_UPDATE=1` pre-update backup support to `scripts/update.sh`. +- Expanded deployment docs for queue workers, scheduler, PHP-FPM pool expectations, Redis migration, backup/export workflow, health checks, log rotation, storage scaling, and database scaling. +- Deferred cached rating/reputation aggregates until real usage identifies slow pages and expensive queries. + ## Phase 15 - UX Polish And Accessibility Pass Purpose: @@ -751,8 +761,8 @@ Provider-protection rules: ## Recommended Immediate Sequence -1. Phase 14A: deployment/scaling hardening. -2. Phase 15A: UX polish and accessibility pass. +1. Phase 15A: UX polish and accessibility pass. +2. Cached rating/reputation aggregates after slow pages are measured. 3. Future mobile/API expansion after native app requirements are clearer. 4. Future email/push delivery implementation after the sender and push providers are explicitly selected. diff --git a/docs/INSTALL.md b/docs/INSTALL.md index bd914ad..799c141 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -22,6 +22,12 @@ From the app directory: bash scripts/update.sh ``` +To create a database/storage backup immediately before updating: + +```bash +TSE_BACKUP_BEFORE_UPDATE=1 bash scripts/update.sh +``` + Or from anywhere: ```bash @@ -35,12 +41,29 @@ The preferred working loop for this project is: 3. SSH to the server and run `bash scripts/update.sh` from the deployed Git checkout. 4. Smoke test the live URL after the update completes. +## Backups And Health Checks + +Create an on-demand backup from the app directory: + +```bash +bash scripts/backup.sh +``` + +Backups default to `storage/app/backups`. Set `TSE_BACKUP_DIR=/path/to/backups` to store them elsewhere. Set `TSE_BACKUP_STORAGE=0` to back up only the database. + +Run a deployment health check from the app directory: + +```bash +bash scripts/health-check.sh +``` + ## Important Notes - The installer is a first-pass Ubuntu/Nginx helper, not a universal hosting panel installer. - Local and CI test runners need PHP SQLite and upload-test support: `pdo_sqlite`, `sqlite3`, `fileinfo`, and `gd`. - Public attachment serving needs `php artisan storage:link`; the checked-in update script runs this automatically. - Tune upload policy with `TSE_ATTACHMENT_DISK`, `TSE_ATTACHMENT_ROOT`, `TSE_ATTACHMENT_MAX_KB`, and `TSE_ATTACHMENT_MIME_TYPES`. +- Tune backup policy with `TSE_BACKUP_DIR`, `TSE_BACKUP_STORAGE`, and optional `TSE_BACKUP_BEFORE_UPDATE=1`. - Review `.env` before production use. - Configure real mail before inviting users. - Move sessions, cache, and queues to Redis before serious traffic. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 80ea68d..5488c37 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -61,11 +61,11 @@ Provider Exchange is a Laravel application for a provider-centered field-service ## Phase Checkpoint -Phases 1-13A have been implemented locally through notification preferences, moderation reporting, and audit logging. The next active planning block is: +Phases 1-14A have been implemented locally through deployment/scaling hardening. The next active planning block is: -8. Deployment/scaling hardening. -9. UX/accessibility polish. -10. Future email/push delivery and native-app API expansion once providers are selected. +1. UX/accessibility polish. +2. Cached reputation aggregates after real slow pages are measured. +3. Future email/push delivery and native-app API expansion once providers are selected. Work-order safeguards to carry into the next implementation passes: @@ -157,6 +157,15 @@ Phase 12A/13A notification and admin governance completed: - Audit logs record selected operational and moderation events, including review moderation, imported-history verification, attachment deletion, work-order actions, quote acceptance, and mobile API actions. - Admins can review recent audit activity from the admin console. +Phase 14A deployment/scaling hardening completed: + +- Added database indexes for job-board filters, profile filters, quotes, work-order timelines, reviews, ratings, disputes, attachments, imported-history queues, and notification inbox lookups. +- Added `scripts/backup.sh` for SQLite, MySQL/MariaDB, and PostgreSQL database backups with optional `storage/app` archive support. +- Added `scripts/health-check.sh` for deployment smoke checks. +- Added optional `TSE_BACKUP_BEFORE_UPDATE=1` pre-update backups to `scripts/update.sh`. +- Documented queue workers, scheduler, PHP-FPM expectations, Redis migration, backups, health checks, log rotation, storage scaling, and database scaling. +- Kept cached reputation aggregates deferred until real traffic shows which pages need them. + Available-work safeguards to carry into directory and job-list phases: - Available jobs should expose pay type, provider terms, schedule type, work type, rough location, buyer reliability, support/contact certification, scope clarity status, and request/quote count. @@ -181,7 +190,7 @@ The reconciled forward plan from the current codebase is captured in `docs/FORWA - Work-order timelines, dispute activity, messages, and notification tables need careful indexes before large production use. - Realtime chat, websocket presence, mobile push, and geolocation check-ins should be added as deliberate service layers instead of bolted onto normal page requests. - Background imports, image processing, moderation summaries, and reputation recalculation should run through queue workers rather than web requests. -- Production deployments need explicit PHP-FPM pool, Nginx, scheduler, queue worker, backup, and log rotation documentation. +- Production deployments now have explicit PHP-FPM, Nginx, scheduler, queue worker, backup, health-check, and log rotation documentation. These should be revisited after real traffic establishes memory, queue, and slow-query patterns. ## Future/Deferred diff --git a/scripts/backup.sh b/scripts/backup.sh new file mode 100644 index 0000000..87a30e7 --- /dev/null +++ b/scripts/backup.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +set -euo pipefail + +APP_DIR="${APP_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" +BACKUP_DIR="${TSE_BACKUP_DIR:-$APP_DIR/storage/app/backups}" +STAMP="$(date -u +%Y%m%dT%H%M%SZ)" + +cd "$APP_DIR" +mkdir -p "$BACKUP_DIR" + +env_value() { + local key="$1" + local fallback="${2:-}" + if [ -f .env ]; then + local line + line="$(grep -E "^${key}=" .env | tail -n 1 || true)" + if [ -n "$line" ]; then + local value="${line#*=}" + value="${value%\"}" + value="${value#\"}" + printf '%s' "$value" + return + fi + fi + printf '%s' "$fallback" +} + +DB_CONNECTION="$(env_value DB_CONNECTION sqlite)" +DB_DATABASE="$(env_value DB_DATABASE "$APP_DIR/database/database.sqlite")" +DB_HOST="$(env_value DB_HOST 127.0.0.1)" +DB_PORT="$(env_value DB_PORT 3306)" +DB_USERNAME="$(env_value DB_USERNAME root)" +DB_PASSWORD="$(env_value DB_PASSWORD '')" + +MANIFEST="$BACKUP_DIR/tse-backup-$STAMP.manifest.txt" +{ + echo "Technical Services Exchange backup" + echo "created_utc=$STAMP" + echo "app_dir=$APP_DIR" + echo "git_commit=$(git rev-parse --short HEAD 2>/dev/null || echo unknown)" + echo "db_connection=$DB_CONNECTION" +} > "$MANIFEST" + +case "$DB_CONNECTION" in + sqlite) + if [ ! -f "$DB_DATABASE" ]; then + echo "SQLite database not found: $DB_DATABASE" >&2 + exit 1 + fi + DB_BACKUP="$BACKUP_DIR/database-$STAMP.sqlite" + cp "$DB_DATABASE" "$DB_BACKUP" + echo "database_backup=$DB_BACKUP" >> "$MANIFEST" + ;; + mysql|mariadb) + command -v mysqldump >/dev/null 2>&1 || { echo "mysqldump is required for $DB_CONNECTION backups" >&2; exit 1; } + DB_BACKUP="$BACKUP_DIR/database-$STAMP.sql.gz" + MYSQL_PWD="$DB_PASSWORD" mysqldump --single-transaction --quick --host="$DB_HOST" --port="$DB_PORT" --user="$DB_USERNAME" "$DB_DATABASE" | gzip > "$DB_BACKUP" + echo "database_backup=$DB_BACKUP" >> "$MANIFEST" + ;; + pgsql) + command -v pg_dump >/dev/null 2>&1 || { echo "pg_dump is required for pgsql backups" >&2; exit 1; } + DB_BACKUP="$BACKUP_DIR/database-$STAMP.sql.gz" + PGPASSWORD="$DB_PASSWORD" pg_dump --host="$DB_HOST" --port="${DB_PORT:-5432}" --username="$DB_USERNAME" "$DB_DATABASE" | gzip > "$DB_BACKUP" + echo "database_backup=$DB_BACKUP" >> "$MANIFEST" + ;; + *) + echo "Unsupported DB_CONNECTION for backup: $DB_CONNECTION" >&2 + exit 1 + ;; +esac + +if [ "${TSE_BACKUP_STORAGE:-1}" = "1" ] && [ -d storage/app ]; then + STORAGE_BACKUP="$BACKUP_DIR/storage-app-$STAMP.tar.gz" + tar --exclude="$BACKUP_DIR" --exclude="storage/app/backups" -czf "$STORAGE_BACKUP" -C "$APP_DIR" storage/app + echo "storage_backup=$STORAGE_BACKUP" >> "$MANIFEST" +fi + +echo "manifest=$MANIFEST" +echo "Backup complete." diff --git a/scripts/health-check.sh b/scripts/health-check.sh new file mode 100644 index 0000000..3a7c344 --- /dev/null +++ b/scripts/health-check.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +set -euo pipefail + +APP_DIR="${APP_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" + +cd "$APP_DIR" + +fail() { + echo "FAIL: $1" >&2 + exit 1 +} + +pass() { + echo "OK: $1" +} + +[ -f artisan ] || fail "artisan not found in $APP_DIR" +[ -f .env ] || fail ".env is missing" +[ -d storage ] || fail "storage directory is missing" +[ -w storage ] || fail "storage directory is not writable" +[ -d bootstrap/cache ] || fail "bootstrap/cache directory is missing" +[ -w bootstrap/cache ] || fail "bootstrap/cache directory is not writable" + +php artisan about --only=environment >/dev/null || fail "Laravel cannot boot" +pass "Laravel boots" + +php artisan migrate:status >/dev/null || fail "Database connection or migrations failed" +pass "Database and migrations are reachable" + +php artisan route:list --path=api/v1 >/dev/null || fail "API routes are not available" +pass "API routes are available" + +php artisan route:list --name=notifications >/dev/null || fail "Notification routes are not available" +pass "Notification routes are available" + +if [ -L public/storage ] || [ -d public/storage ]; then + pass "public storage link exists" +else + fail "public/storage link is missing; run php artisan storage:link" +fi + +if [ -d public/build ]; then + pass "Vite build assets exist" +else + fail "public/build is missing; run npm run build" +fi + +echo "Health check complete." diff --git a/scripts/update.sh b/scripts/update.sh index e3f87e0..a724199 100644 --- a/scripts/update.sh +++ b/scripts/update.sh @@ -5,6 +5,10 @@ APP_DIR="${APP_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" cd "$APP_DIR" +if [ "${TSE_BACKUP_BEFORE_UPDATE:-0}" = "1" ]; then + bash scripts/backup.sh +fi + git pull --ff-only composer install --no-dev --prefer-dist --optimize-autoloader --no-interaction npm ci