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
This commit is contained in:
root
2026-07-09 13:46:51 +02:00
parent ae2f38d6cf
commit d9bf861960
6 changed files with 281 additions and 0 deletions

View File

@@ -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;"]

View File

@@ -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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 KiB

20
nginx.conf Normal file
View File

@@ -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";
}
}

View File

@@ -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()