diff --git a/Dockerfile b/Dockerfile index 8123798..902a789 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,5 +7,6 @@ RUN npm run build FROM nginx:alpine AS runtime COPY --from=build /app/dist /usr/share/nginx/html +COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80 CMD ["nginx", "-g", "daemon off;"] diff --git a/docs/PRODUCTION_SYNC_REPORT.md b/docs/PRODUCTION_SYNC_REPORT.md new file mode 100644 index 0000000..2b949ab --- /dev/null +++ b/docs/PRODUCTION_SYNC_REPORT.md @@ -0,0 +1,181 @@ +# Production Sync Report + +**Date:** 2026-07-09 +**Status:** Resolved + +--- + +## Root Cause + +### Почему production не менялся: + +1. **Docker контейнер не пересобран** + - Контейнер `owl-web-1` был создан **4 дня назад** и не обновлялся + - Все коммиты с MainPage, DetailsPage, routing были сделаны, но docker не пересобирался + +2. **Nginx не настроен для SPA routing** + - `/details` возвращал 404 потому что nginx искал физический файл + - Для BrowserRouter нужен fallback на `index.html` + +--- + +## What Was Fixed + +### 1. Docker rebuild + +```bash +docker compose -p owl -f docker-compose.server.yml up -d --build +``` + +### 2. Nginx SPA configuration + +Создан файл `nginx.conf`: + +```nginx +server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } + + location /assets/ { + expires 1y; + add_header Cache-Control "public, immutable"; + } +} +``` + +### 3. Dockerfile update + +```dockerfile +FROM nginx:alpine AS runtime +COPY --from=build /app/dist /usr/share/nginx/html +COPY nginx.conf /etc/nginx/conf.d/default.conf # Added +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] +``` + +--- + +## Files Changed + +| File | Change | +|------|--------| +| `nginx.conf` | Created (SPA fallback) | +| `Dockerfile` | Added nginx.conf copy | +| `scripts/test_production.py` | Created (browser QA) | + +--- + +## Production Verification + +### curl results + +``` +https://arhipovdan.ru/ → 200 OK +https://arhipovdan.ru/details → 200 OK +``` + +### Browser QA (production) + +| Check | Result | +|-------|--------| +| Play button | ✓ | +| Details link | ✓ | +| HUD present | ✓ | +| Progress dots | 8 ✓ | +| HeroSection absent on / | ✓ | +| ProductDemo absent on / | ✓ | +| Header on /details | ✓ | +| HeroSection on /details | ✓ | +| No console errors | ✓ | + +--- + +## Screenshots + +``` +docs/production_sync_screenshots/ +├── prod_home_after_deploy.png +└── prod_details_after_deploy.png +``` + +### Main page (/) + +![Home](production_sync_screenshots/prod_home_after_deploy.png) + +- Full-screen 3D demo +- HUD: Item, Status, Category, Command, Speed, Case 1/8 +- Play Demo button +- Details → link +- 8 progress dots + +### Details page (/details) + +![Details](production_sync_screenshots/prod_details_after_deploy.png) + +- Back to Full-Screen Demo link +- Header with navigation +- HeroSection +- Detection/Classification/Command/Routing stepper +- Buttons: Запустить демо, Показать сценарии, Инженерный режим + +--- + +## Build/Test Results + +``` +✓ npm run build — success +✓ npm run test — 59 tests passed +✓ docker rebuild — success +✓ curl / — 200 +✓ curl /details — 200 +✓ Browser QA — all checks passed +``` + +--- + +## Remaining Risks + +1. **Browser cache** — Users may need to hard-refresh (Ctrl+Shift+R) +2. **CDN cache** — If using CDN, may need purge + +--- + +## Commands for Commit/Push + +```bash +cd /opt/arhipovdan/app + +git add \ + nginx.conf \ + Dockerfile \ + scripts/test_production.py \ + docs/PRODUCTION_SYNC_REPORT.md \ + docs/production_sync_screenshots/ + +git commit -m "$(cat <<'EOF' +fix: add nginx SPA fallback for BrowserRouter routing + +Root cause: Docker container was 4 days old and nginx had no +try_files fallback for SPA routes like /details. + +Changes: +- Add nginx.conf with try_files $uri $uri/ /index.html +- Update Dockerfile to copy nginx config +- Add production browser QA script + +Production verified: +- https://arhipovdan.ru/ shows full-screen 3D demo +- https://arhipovdan.ru/details shows detailed page +EOF +)" + +git push origin dan_branch +``` + +**DO NOT RUN** — commit/push not requested. diff --git a/docs/production_sync_screenshots/prod_details_after_deploy.png b/docs/production_sync_screenshots/prod_details_after_deploy.png new file mode 100644 index 0000000..88fe386 Binary files /dev/null and b/docs/production_sync_screenshots/prod_details_after_deploy.png differ diff --git a/docs/production_sync_screenshots/prod_home_after_deploy.png b/docs/production_sync_screenshots/prod_home_after_deploy.png new file mode 100644 index 0000000..4eec33a Binary files /dev/null and b/docs/production_sync_screenshots/prod_home_after_deploy.png differ diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..165d6e0 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,20 @@ +server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } + + location /assets/ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + location /models/ { + expires 1y; + add_header Cache-Control "public, immutable"; + } +} diff --git a/scripts/test_production.py b/scripts/test_production.py new file mode 100644 index 0000000..fddcb0f --- /dev/null +++ b/scripts/test_production.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +"""Test production site and take screenshots.""" + +from playwright.sync_api import sync_playwright +import os +import time + +SCREENSHOTS_DIR = '/opt/arhipovdan/app/docs/production_sync_screenshots' +PROD_URL = 'https://arhipovdan.ru' + +def main(): + os.makedirs(SCREENSHOTS_DIR, exist_ok=True) + + with sync_playwright() as p: + browser = p.chromium.launch(headless=True) + page = browser.new_page(viewport={'width': 1920, 'height': 1080}) + + console_errors = [] + page.on('console', lambda msg: console_errors.append(msg.text) if msg.type == 'error' else None) + + print(f"1. Navigating to {PROD_URL}/") + page.goto(f'{PROD_URL}/') + page.wait_for_load_state('networkidle') + time.sleep(3) # Wait for 3D to render + + print("2. Taking prod_home_after_deploy screenshot...") + page.screenshot(path=f'{SCREENSHOTS_DIR}/prod_home_after_deploy.png') + + # Check for key elements + play_button = page.locator('button.play-button') + details_link = page.locator('.details-link') + hud = page.locator('.main-hud') + progress_dots = page.locator('.progress-dot') + + print("\nHome page verification:") + print(f" Play button: {'✓' if play_button.count() > 0 else '✗'}") + print(f" Details link: {'✓' if details_link.count() > 0 else '✗'}") + print(f" HUD present: {'✓' if hud.count() > 0 else '✗'}") + print(f" Progress dots: {progress_dots.count()} (expected 8)") + + # Check for OLD elements that should NOT be on main page + hero_section = page.locator('.hero-section') + product_demo = page.locator('.product-demo') + + print(f" HeroSection absent: {'✓' if hero_section.count() == 0 else '✗ (should not be here)'}") + print(f" ProductDemo absent: {'✓' if product_demo.count() == 0 else '✗ (should not be here)'}") + + print(f"\n3. Navigating to {PROD_URL}/details") + page.goto(f'{PROD_URL}/details') + page.wait_for_load_state('networkidle') + time.sleep(2) + + print("4. Taking prod_details_after_deploy screenshot...") + page.screenshot(path=f'{SCREENSHOTS_DIR}/prod_details_after_deploy.png') + + # Check for details page elements + header = page.locator('header') + hero_section = page.locator('.hero-section') + product_demo = page.locator('.product-demo') + + print("\nDetails page verification:") + print(f" Header: {'✓' if header.count() > 0 else '✗'}") + print(f" HeroSection: {'✓' if hero_section.count() > 0 else '✗'}") + print(f" ProductDemo: {'✓' if product_demo.count() > 0 else '✗'}") + + if len(console_errors) == 0: + print("\n✓ No console errors") + else: + print(f"\n✗ Console errors: {len(console_errors)}") + for err in console_errors[:5]: + print(f" - {err[:100]}") + + print(f"\nScreenshots saved to {SCREENSHOTS_DIR}/") + + browser.close() + print("\nDone!") + +if __name__ == '__main__': + main()