diff --git a/docs/INFRA.md b/docs/INFRA.md
index 19fb2d4..eead148 100644
--- a/docs/INFRA.md
+++ b/docs/INFRA.md
@@ -182,7 +182,7 @@ Mounted copy: `tmp/FE_gentoo/etc/nginx/nginx.conf`.
| `server_name` | `location /` | androidcast crashes | Hub `/app/androidcast_project/` |
|---------------|--------------|---------------------|--------------------------------|
| **`apps.f0xx.org`** | → `http://artc0:80` | Must **not** → `https://artc0:443/...` (static download) | Hub/crashes blocks must use **`http://artc0:80`** — see `nginx.fe-apps.f0xx.org.snippet` |
-| **`/v0/ota/`** on FE | → **`http://artc0:80`** | BE :80 serves static tree | Without this, public OTA is **HTTP 400** while BE localhost is 200 — run `install-fe-ota-nginx.sh` on FE |
+| **`/v0/ota/`** on FE | → **`http://artc0:80`** | BE :80 serves static tree | In consolidated **`/etc/nginx/apps.conf`** — see `patches/fe-nginx-APPLY.md` |
| **`artc0.f0xx.org`** | → `http://artc0:80` | Legacy → **:8089** | Legacy → **:8089** — **remove** hub block; prefer apps.f0xx.org |
**Agent default:** fix **BE `listen 80`** first. Change FE only when user asks or to delete wrong **8089** hub overrides on `artc0.f0xx.org`.
diff --git a/examples/app_hub/assets/i18n/hub-en.json b/examples/app_hub/assets/i18n/hub-en.json
index 2464a9a..8dbb114 100644
--- a/examples/app_hub/assets/i18n/hub-en.json
+++ b/examples/app_hub/assets/i18n/hub-en.json
@@ -8,7 +8,9 @@
"nav.culture": "Culture",
"nav.pricing": "Pricing",
"nav.contact": "Contact us",
+ "nav.console": "Console",
"nav.git": "Git",
+ "nav.issues": "Issues",
"nav.tickets": "Tickets",
"nav.crashes": "Crashes",
"nav.graphs": "Graphs",
diff --git a/examples/app_hub/assets/i18n/hub-ru.json b/examples/app_hub/assets/i18n/hub-ru.json
index 215bad6..21bc57c 100644
--- a/examples/app_hub/assets/i18n/hub-ru.json
+++ b/examples/app_hub/assets/i18n/hub-ru.json
@@ -8,7 +8,9 @@
"nav.culture": "Культура",
"nav.pricing": "Цены",
"nav.contact": "Контакты",
+ "nav.console": "Консоль",
"nav.git": "Git",
+ "nav.issues": "Проблемы",
"nav.tickets": "Тикеты",
"nav.crashes": "Крэши",
"nav.graphs": "Графики",
diff --git a/examples/app_hub/index.php b/examples/app_hub/index.php
index fe4e52b..bdda573 100644
--- a/examples/app_hub/index.php
+++ b/examples/app_hub/index.php
@@ -137,11 +137,12 @@ function hub_h(string $s): string {
diff --git a/examples/crash_reporter/backend/nginx.fe-apps.conf.fragment b/examples/crash_reporter/backend/nginx.fe-apps.conf.fragment
index 92296f4..7cebdb0 100644
--- a/examples/crash_reporter/backend/nginx.fe-apps.conf.fragment
+++ b/examples/crash_reporter/backend/nginx.fe-apps.conf.fragment
@@ -1,21 +1,3 @@
-# FE apps.f0xx.org — include from nginx.conf server block.
-# Build must use the same upstream style as hub/crashes (BE :443), not :80.
-
- # Redirect without trailing slash
- location = /app/androidcast_project/build {
- return 301 $scheme://$host/app/androidcast_project/build/;
- }
- location /app/androidcast_project/build/ {
- proxy_pass https://artc0.intra.raptor.org/app/androidcast_project/build/;
- proxy_http_version 1.1;
- proxy_set_header Host $host;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto $scheme;
- proxy_set_header X-Forwarded-Host $host;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection $connection_upgrade;
- proxy_read_timeout 1800s;
- proxy_send_timeout 1800s;
- client_max_body_size 512m;
- }
+# DEPRECATED — use nginx/fe/apps.conf (full apps.f0xx.org vhost).
+# nginx.conf should contain only: include /etc/nginx/apps.conf;
+# See patches/fe-nginx-APPLY.md and scripts/generate-fe-nginx-patch.sh
diff --git a/examples/crash_reporter/backend/nginx/fe/apps.conf b/examples/crash_reporter/backend/nginx/fe/apps.conf
new file mode 100644
index 0000000..b2a8068
--- /dev/null
+++ b/examples/crash_reporter/backend/nginx/fe/apps.conf
@@ -0,0 +1,137 @@
+# apps.f0xx.org — Android Cast public vhost (FE Gentoo).
+# Included from nginx.conf: include /etc/nginx/apps.conf;
+#
+# Upstream: BE nginx on artc0.intra.raptor.org:80 (hub PHP, crashes, graphs, build, OTA).
+# Git browser: BE :3000 (Gitea). Do not proxy androidcast to BE :443 or :8089.
+#
+# Reload: nginx -t && rc-service nginx reload
+
+server {
+ listen 80;
+ server_name apps.f0xx.org;
+ return 301 https://$host$request_uri;
+}
+
+server {
+ listen 443 ssl http2;
+ server_name apps.f0xx.org;
+
+ access_log /var/log/nginx/apps.intra.raptor.org.access_log main;
+ error_log /var/log/nginx/apps.intra.raptor.org.error_log info;
+
+ ssl_certificate /etc/letsencrypt/live/apps.f0xx.org/fullchain.pem;
+ ssl_certificate_key /etc/letsencrypt/live/apps.f0xx.org/privkey.pem;
+
+ # --- OTA artifacts (BE static tree on listen 80) ---
+ location ^~ /v0/ota/ {
+ proxy_pass http://artc0.intra.raptor.org:80;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+
+ # --- APK builder (long-running Docker builds) ---
+ location = /app/androidcast_project/build {
+ return 301 $scheme://$host/app/androidcast_project/build/;
+ }
+ location ^~ /app/androidcast_project/build/ {
+ proxy_pass http://artc0.intra.raptor.org:80;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_set_header X-Forwarded-Host $host;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection $connection_upgrade;
+ proxy_read_timeout 1800s;
+ proxy_send_timeout 1800s;
+ client_max_body_size 512m;
+ }
+
+ # --- Crash / ticket console ---
+ location = /app/androidcast_project/crashes {
+ return 301 $scheme://$host/app/androidcast_project/crashes/;
+ }
+ location ^~ /app/androidcast_project/crashes/ {
+ proxy_pass http://artc0.intra.raptor.org:80;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ client_max_body_size 16m;
+ }
+
+ # --- Graphs dashboard ---
+ location = /app/androidcast_project/graphs {
+ return 301 $scheme://$host/app/androidcast_project/graphs/;
+ }
+ location ^~ /app/androidcast_project/graphs/ {
+ proxy_pass http://artc0.intra.raptor.org:80;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+
+ # --- Hub landing (PHP index) ---
+ location = /app/androidcast_project {
+ return 301 $scheme://$host/app/androidcast_project/;
+ }
+ location ^~ /app/androidcast_project/ {
+ proxy_pass http://artc0.intra.raptor.org:80;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+
+ # --- Gitea web UI ---
+ location = /app/androidcast_project/git {
+ return 301 $scheme://$host/app/androidcast_project/git/;
+ }
+ location ^~ /app/androidcast_project/git/ {
+ proxy_pass http://artc0.intra.raptor.org:3000/;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_set_header X-Forwarded-Host $host;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection $connection_upgrade;
+ proxy_read_timeout 1800s;
+ proxy_send_timeout 1800s;
+ client_max_body_size 512m;
+ }
+
+ # --- Default: anything else on this vhost → BE :80 ---
+ location / {
+ proxy_pass http://artc0.intra.raptor.org:80;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_connect_timeout 120s;
+ proxy_send_timeout 120s;
+ proxy_read_timeout 180s;
+ proxy_buffers 16 16m;
+ proxy_buffer_size 16m;
+ client_body_buffer_size 16m;
+ client_max_body_size 16m;
+ proxy_buffering off;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection $connection_upgrade;
+ proxy_cache_bypass $http_upgrade;
+ }
+
+ location ~ /\.ht {
+ deny all;
+ }
+}
diff --git a/examples/crash_reporter/backend/patches/fe-nginx-APPLY.md b/examples/crash_reporter/backend/patches/fe-nginx-APPLY.md
new file mode 100644
index 0000000..50dd783
--- /dev/null
+++ b/examples/crash_reporter/backend/patches/fe-nginx-APPLY.md
@@ -0,0 +1,42 @@
+# FE nginx consolidation — review & apply
+
+## What changes
+
+- **`/etc/nginx/apps.conf`** becomes the full `apps.f0xx.org` vhost (HTTP redirect + HTTPS server + all Android Cast locations).
+- **`/etc/nginx/nginx.conf`** drops inline `apps.f0xx.org` server blocks; adds one `include /etc/nginx/apps.conf;` at `http` level.
+
+## New / consolidated paths in apps.conf
+
+| Path | Upstream |
+|------|----------|
+| `/v0/ota/` | BE :80 (OTA) |
+| `/app/androidcast_project/build/` | BE :80 |
+| `/app/androidcast_project/crashes/` | BE :80 |
+| `/app/androidcast_project/graphs/` | BE :80 (explicit) |
+| `/app/androidcast_project/` | BE :80 (hub) |
+| `/app/androidcast_project/git/` | BE :3000 (Gitea) |
+| `/` (default) | BE :80 |
+
+Canonical template: `examples/crash_reporter/backend/nginx/fe/apps.conf`
+
+## Apply (on f0xx-monstro)
+
+```bash
+cd /etc/nginx
+cp -a apps.conf "apps.conf.bak.$(date +%Y%m%d)"
+cp -a nginx.conf "nginx.conf.bak.$(date +%Y%m%d)"
+# copy template from repo sync, or patch:
+# patch -p1 < .../fe-nginx-apps-consolidation.patch
+cp /path/to/android_cast/examples/crash_reporter/backend/nginx/fe/apps.conf apps.conf
+# edit nginx.conf: replace apps.f0xx.org server {…} blocks with:
+# include /etc/nginx/apps.conf;
+nginx -t && rc-service nginx reload
+```
+
+## Verify
+
+```bash
+curl -sS -o /dev/null -w 'hub %{http_code}\n' https://apps.f0xx.org/app/androidcast_project/
+curl -sS -o /dev/null -w 'ota %{http_code}\n' https://apps.f0xx.org/v0/ota/channel/stable.json
+curl -sS -o /dev/null -w 'build %{http_code}\n' https://apps.f0xx.org/app/androidcast_project/build/
+```
diff --git a/examples/crash_reporter/backend/patches/fe-nginx-apps-consolidation.patch b/examples/crash_reporter/backend/patches/fe-nginx-apps-consolidation.patch
new file mode 100644
index 0000000..554b812
--- /dev/null
+++ b/examples/crash_reporter/backend/patches/fe-nginx-apps-consolidation.patch
@@ -0,0 +1,271 @@
+--- a/etc/nginx/apps.conf 2026-06-10 14:42:23.706574376 +0200
++++ b/etc/nginx/apps.conf 2026-06-10 14:42:23.714746551 +0200
+@@ -1,23 +1,137 @@
+-# location /app/androidcast_project/build/ {
+-# proxy_pass https://artc0.intra.raptor.org/app/androidcast_project/build/;
+-# }
+-
+- # Redirect without trailing slash
+- location = /app/androidcast_project/build {
+- return 301 $scheme://$host/app/androidcast_project/build/;
+- }
+- location /app/androidcast_project/build/ {
+- proxy_pass http://artc0.intra.raptor.org:80;
+- proxy_http_version 1.1;
+- proxy_set_header Host $host;
+- proxy_set_header X-Real-IP $remote_addr;
+- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+- proxy_set_header X-Forwarded-Proto $scheme;
+- proxy_set_header X-Forwarded-Host $host;
+- proxy_set_header Upgrade $http_upgrade;
+- proxy_set_header Connection $connection_upgrade;
+- proxy_read_timeout 1800s;
+- proxy_send_timeout 1800s;
+- client_max_body_size 512m;
+- }
++# apps.f0xx.org — Android Cast public vhost (FE Gentoo).
++# Included from nginx.conf: include /etc/nginx/apps.conf;
++#
++# Upstream: BE nginx on artc0.intra.raptor.org:80 (hub PHP, crashes, graphs, build, OTA).
++# Git browser: BE :3000 (Gitea). Do not proxy androidcast to BE :443 or :8089.
++#
++# Reload: nginx -t && rc-service nginx reload
+
++server {
++ listen 80;
++ server_name apps.f0xx.org;
++ return 301 https://$host$request_uri;
++}
++
++server {
++ listen 443 ssl http2;
++ server_name apps.f0xx.org;
++
++ access_log /var/log/nginx/apps.intra.raptor.org.access_log main;
++ error_log /var/log/nginx/apps.intra.raptor.org.error_log info;
++
++ ssl_certificate /etc/letsencrypt/live/apps.f0xx.org/fullchain.pem;
++ ssl_certificate_key /etc/letsencrypt/live/apps.f0xx.org/privkey.pem;
++
++ # --- OTA artifacts (BE static tree on listen 80) ---
++ location ^~ /v0/ota/ {
++ proxy_pass http://artc0.intra.raptor.org:80;
++ proxy_http_version 1.1;
++ proxy_set_header Host $host;
++ proxy_set_header X-Real-IP $remote_addr;
++ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
++ proxy_set_header X-Forwarded-Proto $scheme;
++ }
++
++ # --- APK builder (long-running Docker builds) ---
++ location = /app/androidcast_project/build {
++ return 301 $scheme://$host/app/androidcast_project/build/;
++ }
++ location ^~ /app/androidcast_project/build/ {
++ proxy_pass http://artc0.intra.raptor.org:80;
++ proxy_http_version 1.1;
++ proxy_set_header Host $host;
++ proxy_set_header X-Real-IP $remote_addr;
++ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
++ proxy_set_header X-Forwarded-Proto $scheme;
++ proxy_set_header X-Forwarded-Host $host;
++ proxy_set_header Upgrade $http_upgrade;
++ proxy_set_header Connection $connection_upgrade;
++ proxy_read_timeout 1800s;
++ proxy_send_timeout 1800s;
++ client_max_body_size 512m;
++ }
++
++ # --- Crash / ticket console ---
++ location = /app/androidcast_project/crashes {
++ return 301 $scheme://$host/app/androidcast_project/crashes/;
++ }
++ location ^~ /app/androidcast_project/crashes/ {
++ proxy_pass http://artc0.intra.raptor.org:80;
++ proxy_http_version 1.1;
++ proxy_set_header Host $host;
++ proxy_set_header X-Real-IP $remote_addr;
++ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
++ proxy_set_header X-Forwarded-Proto $scheme;
++ client_max_body_size 16m;
++ }
++
++ # --- Graphs dashboard ---
++ location = /app/androidcast_project/graphs {
++ return 301 $scheme://$host/app/androidcast_project/graphs/;
++ }
++ location ^~ /app/androidcast_project/graphs/ {
++ proxy_pass http://artc0.intra.raptor.org:80;
++ proxy_http_version 1.1;
++ proxy_set_header Host $host;
++ proxy_set_header X-Real-IP $remote_addr;
++ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
++ proxy_set_header X-Forwarded-Proto $scheme;
++ }
++
++ # --- Hub landing (PHP index) ---
++ location = /app/androidcast_project {
++ return 301 $scheme://$host/app/androidcast_project/;
++ }
++ location ^~ /app/androidcast_project/ {
++ proxy_pass http://artc0.intra.raptor.org:80;
++ proxy_http_version 1.1;
++ proxy_set_header Host $host;
++ proxy_set_header X-Real-IP $remote_addr;
++ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
++ proxy_set_header X-Forwarded-Proto $scheme;
++ }
++
++ # --- Gitea web UI ---
++ location = /app/androidcast_project/git {
++ return 301 $scheme://$host/app/androidcast_project/git/;
++ }
++ location ^~ /app/androidcast_project/git/ {
++ proxy_pass http://artc0.intra.raptor.org:3000/;
++ proxy_http_version 1.1;
++ proxy_set_header Host $host;
++ proxy_set_header X-Real-IP $remote_addr;
++ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
++ proxy_set_header X-Forwarded-Proto $scheme;
++ proxy_set_header X-Forwarded-Host $host;
++ proxy_set_header Upgrade $http_upgrade;
++ proxy_set_header Connection $connection_upgrade;
++ proxy_read_timeout 1800s;
++ proxy_send_timeout 1800s;
++ client_max_body_size 512m;
++ }
++
++ # --- Default: anything else on this vhost → BE :80 ---
++ location / {
++ proxy_pass http://artc0.intra.raptor.org:80;
++ proxy_http_version 1.1;
++ proxy_set_header Host $host;
++ proxy_set_header X-Real-IP $remote_addr;
++ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
++ proxy_set_header X-Forwarded-Proto $scheme;
++ proxy_connect_timeout 120s;
++ proxy_send_timeout 120s;
++ proxy_read_timeout 180s;
++ proxy_buffers 16 16m;
++ proxy_buffer_size 16m;
++ client_body_buffer_size 16m;
++ client_max_body_size 16m;
++ proxy_buffering off;
++ proxy_set_header Upgrade $http_upgrade;
++ proxy_set_header Connection $connection_upgrade;
++ proxy_cache_bypass $http_upgrade;
++ }
++
++ location ~ /\.ht {
++ deny all;
++ }
++}
+--- a/etc/nginx/nginx.conf 2026-06-10 14:42:23.656573680 +0200
++++ b/etc/nginx/nginx.conf 2026-06-10 14:42:23.745801792 +0200
+@@ -918,104 +918,8 @@
+
+
+ ## -------------------------------------------------------------------------------
++## apps.f0xx.org — Android Cast (hub, crashes, graphs, build, OTA, git)
++ include /etc/nginx/apps.conf;
+
+- server {
+- listen 80;
+- server_name apps.f0xx.org;
+- location / {
+- rewrite ^(.*)$ https://apps.f0xx.org$1 permanent;
+- }
+- # redirects both www and non-www to https
+- return 301 https://apps.f0xx.org$request_uri;
+- }
+- server {
+- listen 443 ssl http2;
+- server_name apps.f0xx.org;
+-
+- access_log /var/log/nginx/apps.intra.raptor.org.access_log main;
+- error_log /var/log/nginx/apps.intra.raptor.org.error_log info;
+-
+- ssl_certificate /etc/letsencrypt/live/apps.f0xx.org/fullchain.pem;
+- ssl_certificate_key /etc/letsencrypt/live/apps.f0xx.org/privkey.pem;
+-
+- location / {
+- proxy_pass http://artc0.intra.raptor.org:80;
+- proxy_set_header Host $host;
+- proxy_set_header X-Real-IP $remote_addr;
+- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+- proxy_set_header X-Forwarded-Proto $scheme;
+- proxy_connect_timeout 120;
+- proxy_send_timeout 120;
+- proxy_read_timeout 180;
+- proxy_buffers 16 16M;
+- proxy_buffer_size 16M;
+- client_body_buffer_size 16M;
+- client_max_body_size 16M;
+- proxy_buffering off;
+- proxy_set_header Upgrade $http_upgrade;
+- proxy_set_header Connection keep-alive;
+- proxy_set_header Host $host;
+- proxy_cache_bypass $http_upgrade;
+- proxy_set_header Connection "upgrade";
+- }
+-
+- #location /app/androidcast_project/ {
+- # proxy_pass https://artc0.intra.raptor.org/app/androidcast_project/;
+- #}
+-#
+-# location /app/androidcast_project/crashes/ {
+-# proxy_pass https://artc0.intra.raptor.org/app/androidcast_project/crashes/;
+-# }
+-#
+-
+- location /app/androidcast_project/ {
+- proxy_pass http://artc0.intra.raptor.org:80;
+- proxy_http_version 1.1;
+- proxy_set_header Host $host;
+- proxy_set_header X-Real-IP $remote_addr;
+- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+- proxy_set_header X-Forwarded-Proto $scheme;
+- }
+-
+- location /app/androidcast_project/crashes/ {
+- proxy_pass http://artc0.intra.raptor.org:80;
+- proxy_http_version 1.1;
+- proxy_set_header Host $host;
+- proxy_set_header X-Real-IP $remote_addr;
+- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+- proxy_set_header X-Forwarded-Proto $scheme;
+- }
+-
+- include /etc/nginx/apps.conf;
+-
+- location ~ /\.ht {
+- deny all;
+- }
+-
+- root /var/www/localhost/htdocs/android_cast/project/android_cast/examples/crash_reporter/backend/public/;
+- index index.php;
+-
+-
+-## gitea redirection
+-# Redirect without trailing slash
+- location = /app/androidcast_project/git {
+- return 301 $scheme://$host/app/androidcast_project/git/;
+- }
+- location /app/androidcast_project/git/ {
+- proxy_pass http://artc0.intra.raptor.org:3000/;
+- proxy_http_version 1.1;
+- proxy_set_header Host $host;
+- proxy_set_header X-Real-IP $remote_addr;
+- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+- proxy_set_header X-Forwarded-Proto $scheme;
+- proxy_set_header X-Forwarded-Host $host;
+- proxy_set_header Upgrade $http_upgrade;
+- proxy_set_header Connection $connection_upgrade;
+- proxy_read_timeout 1800s;
+- proxy_send_timeout 1800s;
+- client_max_body_size 512m;
+- }
+-
+- }
+
+ }
diff --git a/examples/crash_reporter/backend/public/assets/css/app.css b/examples/crash_reporter/backend/public/assets/css/app.css
index 19d51b6..0c91591 100644
--- a/examples/crash_reporter/backend/public/assets/css/app.css
+++ b/examples/crash_reporter/backend/public/assets/css/app.css
@@ -1762,6 +1762,19 @@ button.report-tag--filter:hover {
align-items: center;
gap: 8px;
}
+.graphs-session-notice {
+ margin: 0 0 12px;
+ padding: 10px 14px;
+ border-radius: 8px;
+ border: 1px solid rgba(245, 158, 11, 0.45);
+ background: rgba(245, 158, 11, 0.12);
+ color: var(--text);
+ font-size: 0.9rem;
+ line-height: 1.45;
+}
+.graphs-session-notice[hidden] {
+ display: none;
+}
.graph-brick-drag-handle {
flex-shrink: 0;
display: inline-flex;
diff --git a/examples/crash_reporter/backend/public/assets/i18n/en.json b/examples/crash_reporter/backend/public/assets/i18n/en.json
index a2e2a02..3f93fe4 100644
--- a/examples/crash_reporter/backend/public/assets/i18n/en.json
+++ b/examples/crash_reporter/backend/public/assets/i18n/en.json
@@ -16,6 +16,7 @@
"home.card_issues": "Issues",
"home.card_issues_desc": "Browse and triage crash reports, tags, and fingerprints.",
"home.card_new_issue_desc": "Log a new issue or task in one step.",
+ "home.card_new_ticket_desc": "Create a roadmap or QA ticket in one step.",
"home.card_tickets_desc": "Roadmap tasks, QA items, and workflow tags.",
"home.card_graphs": "Graphs",
"home.card_graphs_desc": "Sessions, crashes, and device activity over time.",
@@ -63,6 +64,7 @@
"col.drag": "Drag column",
"col.drag_reorder": "Drag column to reorder",
"graphs.brick_drag": "Drag chart to reorder",
+ "graphs.session_deficit": "Session charts are empty. On the device: Developer settings → enable “Grab session stats”, cast a session, then wait for upload to graph_upload.php.",
"row.show_summary": "Show summary",
"row.open_report": "Click to open full report",
"row.edit_tags": "Edit tags",
diff --git a/examples/crash_reporter/backend/public/assets/i18n/ru.json b/examples/crash_reporter/backend/public/assets/i18n/ru.json
index 378f0f1..c805242 100644
--- a/examples/crash_reporter/backend/public/assets/i18n/ru.json
+++ b/examples/crash_reporter/backend/public/assets/i18n/ru.json
@@ -26,6 +26,7 @@
"home.card_issues": "Проблемы",
"home.card_issues_desc": "Просмотр и разбор отчётов о сбоях, тегов и отпечатков.",
"home.card_new_issue_desc": "Создать новую проблему или задачу.",
+ "home.card_new_ticket_desc": "Создать тикет roadmap или QA за один шаг.",
"home.card_tickets_desc": "Задачи, QA и теги workflow.",
"home.card_graphs": "Графики",
"home.card_graphs_desc": "Сессии, сбои и активность устройств.",
@@ -73,6 +74,7 @@
"col.drag": "Перетащить столбец",
"col.drag_reorder": "Перетащите для смены порядка столбцов",
"graphs.brick_drag": "Перетащите график для смены порядка",
+ "graphs.session_deficit": "Графики сессий пусты. На устройстве: Настройки разработчика → «Grab session stats», проведите сессию трансляции и дождитесь загрузки в graph_upload.php.",
"row.show_summary": "Показать сводку",
"row.open_report": "Нажмите, чтобы открыть полный отчёт",
"row.edit_tags": "Редактировать метки",
diff --git a/examples/crash_reporter/backend/public/assets/js/graphs.js b/examples/crash_reporter/backend/public/assets/js/graphs.js
index 1559b7b..1e92f40 100644
--- a/examples/crash_reporter/backend/public/assets/js/graphs.js
+++ b/examples/crash_reporter/backend/public/assets/js/graphs.js
@@ -1025,9 +1025,10 @@
let statusText = 'Loaded · viewer=' + viewer + ' · window=' + days + 'd · columns=' + cols;
const visibleScope =
viewer === 'platform_admin' ? graphScopes.platform : viewer === 'slug_admin' ? graphScopes.slug : graphScopes.user;
- if (sessionMetricsDeficit(visibleScope)) {
- statusText +=
- ' · Session metrics empty — cast with Developer “Grab session stats” on; app uploads graph_session to graph_upload.php';
+ const deficitNotice = el('graphs-session-notice');
+ const deficit = sessionMetricsDeficit(visibleScope);
+ if (deficitNotice) {
+ deficitNotice.hidden = !deficit;
}
status.textContent = statusText;
openGraphFromPermalink();
diff --git a/examples/crash_reporter/backend/scripts/generate-fe-nginx-patch.sh b/examples/crash_reporter/backend/scripts/generate-fe-nginx-patch.sh
new file mode 100755
index 0000000..630b8e4
--- /dev/null
+++ b/examples/crash_reporter/backend/scripts/generate-fe-nginx-patch.sh
@@ -0,0 +1,128 @@
+#!/usr/bin/env bash
+# Build fe-nginx-apps-consolidation.patch from repo template vs tmp/FE_gentoo mirror.
+set -euo pipefail
+
+REPO_ROOT="$(cd "$(dirname "$0")/../../../.." && pwd)"
+FE_MIRROR="${FE_MIRROR:-$REPO_ROOT/tmp/FE_gentoo/etc/nginx}"
+TEMPLATE="$REPO_ROOT/examples/crash_reporter/backend/nginx/fe/apps.conf"
+PATCH_OUT="$REPO_ROOT/examples/crash_reporter/backend/patches/fe-nginx-apps-consolidation.patch"
+
+if [[ ! -f "$FE_MIRROR/nginx.conf" || ! -f "$FE_MIRROR/apps.conf" ]]; then
+ echo "Missing FE mirror at $FE_MIRROR (sshfs tmp/FE_gentoo)" >&2
+ exit 1
+fi
+
+WORK="$(mktemp -d)"
+trap 'rm -rf "$WORK"' EXIT
+
+cp "$FE_MIRROR/nginx.conf" "$WORK/nginx.conf.orig"
+cp "$FE_MIRROR/apps.conf" "$WORK/apps.conf.orig"
+cp "$TEMPLATE" "$WORK/apps.conf.new"
+
+python3 - "$WORK/nginx.conf.orig" "$WORK/nginx.conf.new" << 'PY'
+import sys
+from pathlib import Path
+
+src, dst = map(Path, sys.argv[1:3])
+lines = src.read_text().splitlines(keepends=True)
+
+start = None
+for i, line in enumerate(lines):
+ if line.strip() == "## -------------------------------------------------------------------------------":
+ # Next non-empty should be server { listen 80 apps.f0xx.org
+ for j in range(i + 1, min(i + 5, len(lines))):
+ if "server_name apps.f0xx.org" in lines[j] or (
+ j + 1 < len(lines) and "server_name apps.f0xx.org" in lines[j + 1]
+ ):
+ start = i
+ break
+ if start is not None:
+ break
+
+if start is None:
+ raise SystemExit("Could not find apps.f0xx.org section start in nginx.conf")
+
+# End at closing brace of 443 server: line that is only `\t}` or ` }` before blank line and http `}`
+end = None
+for i in range(len(lines) - 1, start, -1):
+ if lines[i].strip() == "}":
+ # http block ends with `}\n` then EOF; 443 server closes with `}\n` then `\n` then `}`
+ if i + 1 < len(lines) and lines[i + 1].strip() == "":
+ if i + 2 < len(lines) and lines[i + 2].strip() == "}":
+ end = i
+ break
+
+if end is None:
+ raise SystemExit("Could not find apps.f0xx.org section end in nginx.conf")
+
+replacement = (
+ "## -------------------------------------------------------------------------------\n"
+ "## apps.f0xx.org — Android Cast (hub, crashes, graphs, build, OTA, git)\n"
+ "\tinclude /etc/nginx/apps.conf;\n"
+ "\n"
+)
+new_lines = lines[:start] + [replacement] + lines[end + 1 :]
+dst.write_text("".join(new_lines))
+print(f"nginx.conf: replaced lines {start + 1}–{end + 1} with include")
+PY
+
+mkdir -p "$(dirname "$PATCH_OUT")"
+(
+ cd "$WORK"
+ diff -u apps.conf.orig apps.conf.new || true
+ diff -u nginx.conf.orig nginx.conf.new || true
+) > "$PATCH_OUT" || true
+
+# Fix patch headers to use etc/nginx/ paths for clarity
+sed -i 's|--- .*apps.conf.orig|--- a/etc/nginx/apps.conf|' "$PATCH_OUT"
+sed -i 's|+++ .*apps.conf.new|+++ b/etc/nginx/apps.conf|' "$PATCH_OUT"
+sed -i 's|--- .*nginx.conf.orig|--- a/etc/nginx/nginx.conf|' "$PATCH_OUT"
+sed -i 's|+++ .*nginx.conf.new|+++ b/etc/nginx/nginx.conf|' "$PATCH_OUT"
+
+cat > "$REPO_ROOT/examples/crash_reporter/backend/patches/fe-nginx-APPLY.md" << EOF
+# FE nginx consolidation — review & apply
+
+## What changes
+
+- **\`/etc/nginx/apps.conf\`** becomes the full \`apps.f0xx.org\` vhost (HTTP redirect + HTTPS server + all Android Cast locations).
+- **\`/etc/nginx/nginx.conf\`** drops inline \`apps.f0xx.org\` server blocks; adds one \`include /etc/nginx/apps.conf;\` at \`http\` level.
+
+## New / consolidated paths in apps.conf
+
+| Path | Upstream |
+|------|----------|
+| \`/v0/ota/\` | BE :80 (OTA) |
+| \`/app/androidcast_project/build/\` | BE :80 |
+| \`/app/androidcast_project/crashes/\` | BE :80 |
+| \`/app/androidcast_project/graphs/\` | BE :80 (explicit) |
+| \`/app/androidcast_project/\` | BE :80 (hub) |
+| \`/app/androidcast_project/git/\` | BE :3000 (Gitea) |
+| \`/\` (default) | BE :80 |
+
+Canonical template: \`examples/crash_reporter/backend/nginx/fe/apps.conf\`
+
+## Apply (on f0xx-monstro)
+
+\`\`\`bash
+cd /etc/nginx
+cp -a apps.conf "apps.conf.bak.\$(date +%Y%m%d)"
+cp -a nginx.conf "nginx.conf.bak.\$(date +%Y%m%d)"
+# copy template from repo sync, or patch:
+# patch -p1 < .../fe-nginx-apps-consolidation.patch
+cp /path/to/android_cast/examples/crash_reporter/backend/nginx/fe/apps.conf apps.conf
+# edit nginx.conf: replace apps.f0xx.org server {…} blocks with:
+# include /etc/nginx/apps.conf;
+nginx -t && rc-service nginx reload
+\`\`\`
+
+## Verify
+
+\`\`\`bash
+curl -sS -o /dev/null -w 'hub %{http_code}\\n' https://apps.f0xx.org/app/androidcast_project/
+curl -sS -o /dev/null -w 'ota %{http_code}\\n' https://apps.f0xx.org/v0/ota/channel/stable.json
+curl -sS -o /dev/null -w 'build %{http_code}\\n' https://apps.f0xx.org/app/androidcast_project/build/
+\`\`\`
+EOF
+
+echo "Wrote $PATCH_OUT"
+echo "Wrote $REPO_ROOT/examples/crash_reporter/backend/patches/fe-nginx-APPLY.md"
diff --git a/examples/crash_reporter/backend/scripts/install-fe-ota-nginx.sh b/examples/crash_reporter/backend/scripts/install-fe-ota-nginx.sh
index 79df32d..e5e67f2 100755
--- a/examples/crash_reporter/backend/scripts/install-fe-ota-nginx.sh
+++ b/examples/crash_reporter/backend/scripts/install-fe-ota-nginx.sh
@@ -1,5 +1,6 @@
#!/usr/bin/env bash
# Run on Gentoo FE as root — proxy /v0/ota/ to BE listen 80 (static OTA tree).
+# Superseded by consolidated apps.conf (nginx/fe/apps.conf + fe-nginx-apps-consolidation.patch).
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/../../../.." && pwd)"
diff --git a/examples/crash_reporter/backend/views/layout.php b/examples/crash_reporter/backend/views/layout.php
index f675d98..881729b 100644
--- a/examples/crash_reporter/backend/views/layout.php
+++ b/examples/crash_reporter/backend/views/layout.php
@@ -313,6 +313,9 @@
+
+ Session charts are empty. On the device: Developer settings → enable “Grab session stats”, cast a session, then wait for upload to graph_upload.php.
+
Loading…
diff --git a/examples/crash_reporter/backend/views/partials/console_home_landing.php b/examples/crash_reporter/backend/views/partials/console_home_landing.php
index 8f31352..a8f26fd 100644
--- a/examples/crash_reporter/backend/views/partials/console_home_landing.php
+++ b/examples/crash_reporter/backend/views/partials/console_home_landing.php
@@ -34,6 +34,11 @@ $proj = '/app/androidcast_project';
Tickets
Roadmap tasks, QA items, and workflow tags.
+
+
+ New ticket
+ Create a roadmap or QA ticket in one step.
+
Graphs