- Introduce cache busting (to circumvent aggressive caching on iOS - but ideal in other contexts as well)
- Change the build process to allow cache busting - Optimisations to the build process - Several improvements of UI geared towards mobile experience -
This commit is contained in:
106
documentation/cache-busting-and-builds.md
Normal file
106
documentation/cache-busting-and-builds.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# Cache busting, builds en distributie (Parcel v2 + manifest)
|
||||
|
||||
Dit document beschrijft hoe cache busting nu structureel is geïmplementeerd met Parcel v2 (filename hashing) en hoe je builds en distributie uitvoert in de verschillende omgevingen.
|
||||
|
||||
## Overzicht van de aanpak
|
||||
|
||||
- Parcel v2 bouwt de frontend bundles met content-hashes in de bestandsnamen (bijv. `/static/dist/chat-client.abcd123.js`).
|
||||
- We bouwen via minimale HTML entries (frontend_src/entries/*.html) die de JS importeren; dit dwingt Parcel tot consistente hashing en CSS-extractie.
|
||||
- Na de build schrijven we een `manifest.json` in `nginx/static/dist/` met een mapping van logische namen naar gehashte paden.
|
||||
- Templates gebruiken nu `asset_url('dist/chat-client.js|css')` om automatisch naar de juiste gehashte bestanden te verwijzen.
|
||||
- Nginx (DEV/TEST) is ingesteld om voor `/static/dist/` lange cache headers te sturen: `Cache-Control: public, max-age=31536000, immutable`.
|
||||
- HTML krijgt geen agressieve caching via Nginx; de browser ontdekt bij volgende request de nieuwe gehashte asset-URLs.
|
||||
- In STAGING/PROD (Ingress + Bunny) volg je dezelfde principes; CDN ziet unieke URLs en cachet langdurig, zonder purges te vereisen.
|
||||
|
||||
## Relevante bestanden die zijn aangepast/toegevoegd
|
||||
|
||||
- `nginx/package.json`
|
||||
- DevDependency: `@parcel/reporter-bundle-manifest` toegevoegd.
|
||||
- `postbuild` script toegevoegd (optioneel; puur informatief).
|
||||
- `nginx/.parcelrc`
|
||||
- Parcel reporter geconfigureerd zodat `manifest.json` wordt weggeschreven.
|
||||
- `common/utils/template_filters.py`
|
||||
- Nieuwe Jinja global `asset_url(logical_path)` geregistreerd.
|
||||
- `eveai_chat_client/asset_manifest.py`
|
||||
- Hulpfuncties om manifest in te lezen en paden te resolven met caching.
|
||||
- `eveai_chat_client/templates/base.html` en `templates/scripts.html`
|
||||
- CSS/JS referenties omgezet naar `{{ asset_url('dist/chat-client.css|js') }}`.
|
||||
- `nginx/nginx.conf`
|
||||
- Extra location voor `/static/dist/` met lange cache headers.
|
||||
|
||||
## Build uitvoeren (lokaal of in CI)
|
||||
|
||||
1. Ga naar de `nginx` directory.
|
||||
2. Installeer dependencies (eenmalig of na wijzigingen):
|
||||
- `npm install`
|
||||
3. Run build:
|
||||
- `npm run clean`
|
||||
- `npm run build`
|
||||
|
||||
Resultaat:
|
||||
- Bundles met content-hash in `nginx/static/dist/`
|
||||
- `nginx/static/dist/manifest.json` aanwezig
|
||||
|
||||
Tip: De build wordt ook aangeroepen door het bestaande script `docker/rebuild_chat_client.sh`.
|
||||
|
||||
## rebuild_chat_client.sh
|
||||
|
||||
Script: `docker/rebuild_chat_client.sh`
|
||||
- Kopieert client images naar `nginx/static/assets/img`.
|
||||
- Voert vervolgens `npm run clean` en `npm run build` uit in `nginx/`.
|
||||
- Bouwt en pusht daarna de `nginx` container.
|
||||
|
||||
Door de nieuwe Parcel configuratie zal de build automatisch `manifest.json` genereren en gehashte assets outputten. Er zijn geen extra stappen nodig in dit script.
|
||||
|
||||
## Hoe de templates de juiste bestanden vinden
|
||||
|
||||
- De Jinja global `asset_url()` zoekt in `manifest.json` de gehashte bestandsnaam op basis van een logische naam.
|
||||
- Voorbeelden in templates:
|
||||
- CSS: `{{ asset_url('dist/chat-client.css') }}`
|
||||
- JS: `{{ asset_url('dist/chat-client.js') }}`
|
||||
- Als het manifest onverhoopt ontbreekt (bijv. in een edge-case), valt `asset_url` automatisch terug naar het on-gehashte pad onder `/static/` zodat de pagina niet breekt.
|
||||
|
||||
## Nginx (DEV/TEST)
|
||||
|
||||
In `nginx/nginx.conf` is toegevoegd:
|
||||
|
||||
```
|
||||
location ^~ /static/dist/ {
|
||||
alias /etc/nginx/static/dist/;
|
||||
add_header Cache-Control "public, max-age=31536000, immutable" always;
|
||||
}
|
||||
```
|
||||
|
||||
- Hiermee krijgen gehashte assets lange caching. Omdat de URL wijzigt bij iedere inhoudswijziging, halen browsers/CDN de nieuwe versie zonder problemen op.
|
||||
- HTML wordt niet agressief gecachet door Nginx; wil je volledig no-cache afdwingen voor HTML, dan kun je dat ook applicatie‑zijde doen (Flask) of via extra Nginx rules voor specifieke HTML routes.
|
||||
|
||||
## Ingress (STAGING/PROD) en Bunny.net
|
||||
|
||||
- Ingress: Zet (indien nog niet aanwezig) voor het pad dat `/static/dist/` serveert een configuration snippet met dezelfde header:
|
||||
- `add_header Cache-Control "public, max-age=31536000, immutable" always;`
|
||||
- HTML/endpoints laat je kort/no-cache of standaard; belangrijk is dat templates met nieuwe gehashte URLs snel door de clients worden opgepakt.
|
||||
- Bunny.net (pull zone): CDN cachet op basis van volledige URL. Door de content-hash in de bestandsnaam zijn purges normaliter niet nodig.
|
||||
|
||||
## Troubleshooting / Tips
|
||||
|
||||
- Controleer na build of `manifest.json` bestaat:
|
||||
- `ls nginx/static/dist/manifest.json`
|
||||
- Inspecteer headers in DEV:
|
||||
- `curl -I http://<host>/static/dist/chat-client.<hash>.js`
|
||||
- Moet `Cache-Control: public, max-age=31536000, immutable` tonen.
|
||||
- Als een client toch oude CSS/JS toont:
|
||||
- Check of de geleverde HTML naar de nieuwste gehashte bestandsnaam verwijst (View Source / Network tab).
|
||||
- Zorg dat de HTML niet op CDN langdurig wordt gecachet.
|
||||
|
||||
## Samenvatting workflow per omgeving
|
||||
|
||||
- DEV/TEST (Nginx):
|
||||
1) `docker/rebuild_chat_client.sh` draaien (of handmatig `npm run build` in `nginx/`).
|
||||
2) Nginx container wordt vernieuwd met nieuwe `static/dist` inclusief manifest en gehashte bundles.
|
||||
|
||||
- STAGING/PROD (Ingress + Bunny):
|
||||
1) Zelfde build, gehashte bundles en manifest worden gedeployed via container image.
|
||||
2) Ingress dient `/static/dist/` met lange cache headers.
|
||||
3) Bunny.net cachet de nieuwe URLs automatisch; purgen niet nodig.
|
||||
|
||||
Met deze setup combineren we maximale performance (lange caching) met directe updates (nieuwe URL bij wijziging).
|
||||
208
documentation/migratie_startup_en_builds.md
Normal file
208
documentation/migratie_startup_en_builds.md
Normal file
@@ -0,0 +1,208 @@
|
||||
# Migratieplan: Standaardisatie van Startup Processen en Docker Builds
|
||||
|
||||
Dit document beschrijft de migratie-afspraken rond het opstarten van onze applicaties, inclusief het gebruik van een generiek startscript, het inzetten van `tini` als entrypoint, en de overstap naar een **shared base build** structuur.
|
||||
|
||||
Doel: **standaardisatie**, **betere betrouwbaarheid in Kubernetes**, en **snellere builds**.
|
||||
|
||||
---
|
||||
|
||||
## 1. Generiek startscript (`scripts/start.sh`)
|
||||
|
||||
### Doel
|
||||
- Eén startscript voor **alle rollen**: web, worker, beat.
|
||||
- Gedrag wordt bepaald via **environment variables** (`ROLE=web|worker|beat`).
|
||||
- Geen “magische” verschillen meer tussen Podman en Kubernetes.
|
||||
|
||||
### Voorbeeld
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROLE="${ROLE:-web}"
|
||||
|
||||
case "$ROLE" in
|
||||
web)
|
||||
exec gunicorn -w "${WORKERS:-1}" -k "${WORKER_CLASS:-gevent}" -b "0.0.0.0:${PORT:-8080}" --worker-connections "${WORKER_CONN:-100}" --access-logfile - --error-logfile - --log-level "${LOGLEVEL:-info}" scripts.run_eveai_app:app
|
||||
;;
|
||||
|
||||
worker)
|
||||
exec celery -A scripts.run_eveai_workers worker --loglevel="${CELERY_LOGLEVEL:-INFO}" --concurrency="${CELERY_CONCURRENCY:-2}" --max-tasks-per-child="${CELERY_MAX_TASKS_PER_CHILD:-1000}" --prefetch-multiplier="${CELERY_PREFETCH:-1}" -O fair
|
||||
;;
|
||||
|
||||
beat)
|
||||
exec celery -A scripts.run_eveai_workers beat --loglevel="${CELERY_LOGLEVEL:-INFO}"
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Unknown ROLE=$ROLE" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
```
|
||||
|
||||
### Belangrijk
|
||||
- **Geen init/migraties** meer in het startscript (die draaien we via Jobs/CronJobs).
|
||||
- **Geen `cd`, `chown`, of PYTHONPATH-hacks** in startscript → alles naar Dockerfile.
|
||||
- **Altijd `exec`** gebruiken zodat processen signalen correct ontvangen.
|
||||
|
||||
---
|
||||
|
||||
## 2. Gebruik van `tini` als ENTRYPOINT
|
||||
|
||||
### Waarom?
|
||||
- In containers draait het eerste proces als **PID 1**.
|
||||
- Zonder init-proces worden signalen niet correct doorgegeven en ontstaan **zombieprocessen**.
|
||||
- `tini` is een lichtgewicht init die dit oplost.
|
||||
|
||||
### Implementatie
|
||||
```dockerfile
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends tini && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ENTRYPOINT ["/usr/bin/tini","-g","--"]
|
||||
CMD ["bash","-lc","scripts/start.sh"]
|
||||
```
|
||||
|
||||
- `-g` = stuur signalen naar de hele process group (belangrijk voor Gunicorn en Celery).
|
||||
- Hiermee is een apart `entrypoint.sh` niet meer nodig.
|
||||
|
||||
---
|
||||
|
||||
## 3. Verplaatsen van logica naar Dockerfile
|
||||
|
||||
### Wat naar de Dockerfile moet
|
||||
- `WORKDIR /app`
|
||||
- `ENV PYTHONPATH=/app:/app/patched_packages:$PYTHONPATH`
|
||||
- `ENV FLASK_APP=/app/scripts/run_eveai_app.py` (alleen nodig voor CLI, niet voor gunicorn)
|
||||
- User-aanmaak en permissies:
|
||||
```dockerfile
|
||||
ARG UID=10001
|
||||
ARG GID=10001
|
||||
RUN groupadd -g ${GID} appuser && useradd -u ${UID} -g ${GID} -M -d /nonexistent -s /usr/sbin/nologin appuser
|
||||
RUN chown -R appuser:appuser /app
|
||||
USER appuser
|
||||
```
|
||||
|
||||
### Wat niet meer in startscript hoort
|
||||
- `cd /app`
|
||||
- `export PYTHONPATH=...`
|
||||
- `chown -R ... /logs`
|
||||
- DB-migraties of cache-invalidatie → deze verhuizen naar **Kubernetes Jobs/CronJobs**.
|
||||
|
||||
---
|
||||
|
||||
## 4. Kubernetes Jobs & CronJobs
|
||||
|
||||
### Use cases
|
||||
- **Jobs** → eenmalige taken zoals DB-migraties of cache-invalidatie.
|
||||
- **CronJobs** → geplande taken (bv. nachtelijke opschoningen).
|
||||
|
||||
### Waarom?
|
||||
- Startup scripts blijven lean.
|
||||
- Geen race conditions bij meerdere replicas.
|
||||
- Flexibel los van deploys uit te voeren.
|
||||
|
||||
---
|
||||
|
||||
## 5. Docker builds: naar een shared base (Optie B)
|
||||
|
||||
### Nieuwe structuur
|
||||
```
|
||||
repo/
|
||||
├─ Dockerfile.base
|
||||
├─ requirements.txt
|
||||
├─ common/
|
||||
├─ config/
|
||||
├─ scripts/
|
||||
├─ patched_packages/
|
||||
├─ docker/
|
||||
│ ├─ eveai_app/Dockerfile
|
||||
│ ├─ eveai_api/Dockerfile
|
||||
│ └─ eveai_workers/Dockerfile
|
||||
└─ compose_dev.yaml
|
||||
```
|
||||
|
||||
### Dockerfile.base
|
||||
```dockerfile
|
||||
FROM python:3.12-slim
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends tini && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ARG UID=10001
|
||||
ARG GID=10001
|
||||
RUN groupadd -g ${GID} appuser && useradd -u ${UID} -g ${GID} -M -d /nonexistent -s /usr/sbin/nologin appuser
|
||||
|
||||
WORKDIR /app
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY common /app/common
|
||||
COPY config /app/config
|
||||
COPY scripts /app/scripts
|
||||
COPY patched_packages /app/patched_packages
|
||||
|
||||
RUN chown -R appuser:appuser /app && chmod +x /app/scripts/start.sh
|
||||
|
||||
ENV PYTHONPATH=/app:/app/patched_packages:${PYTHONPATH} ROLE=web PORT=8080
|
||||
|
||||
USER appuser
|
||||
EXPOSE 8080
|
||||
|
||||
ENTRYPOINT ["/usr/bin/tini","-g","--"]
|
||||
CMD ["bash","-lc","scripts/start.sh"]
|
||||
```
|
||||
|
||||
### Service Dockerfiles
|
||||
|
||||
#### docker/eveai_app/Dockerfile
|
||||
```dockerfile
|
||||
FROM yourorg/eveai-base:py312-v1
|
||||
WORKDIR /app
|
||||
COPY eveai_app /app/eveai_app
|
||||
COPY migrations /app/migrations
|
||||
COPY content /app/content
|
||||
ENV ROLE=web PORT=8080
|
||||
```
|
||||
|
||||
#### docker/eveai_api/Dockerfile
|
||||
```dockerfile
|
||||
FROM yourorg/eveai-base:py312-v1
|
||||
WORKDIR /app
|
||||
COPY eveai_api /app/eveai_api
|
||||
ENV ROLE=web PORT=8080
|
||||
```
|
||||
|
||||
#### docker/eveai_workers/Dockerfile
|
||||
```dockerfile
|
||||
FROM yourorg/eveai-base:py312-v1
|
||||
WORKDIR /app
|
||||
COPY eveai_workers /app/eveai_workers
|
||||
ENV ROLE=worker
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Workflow afspraken
|
||||
|
||||
1. **Startscript** is generiek → gedrag via env (`ROLE=...`).
|
||||
2. **Geen init-taken** meer in startscript → Jobs/CronJobs in k8s.
|
||||
3. **`tini` als ENTRYPOINT** → correcte signalen en zombie-cleanup.
|
||||
4. **Dockerfile** regelt PYTHONPATH, FLASK_APP, permissies en user.
|
||||
5. **Base image** bevat Python, deps en common code.
|
||||
6. **Service-images** kopiëren enkel hun eigen directories.
|
||||
7. **Build flow**:
|
||||
- Base builden/pushen bij gewijzigde deps/common.
|
||||
- Services snel erbovenop rebuilden bij code-wijzigingen.
|
||||
|
||||
---
|
||||
|
||||
## 7. Samenvatting
|
||||
|
||||
Met deze migratie krijgen we:
|
||||
- Eenvoudiger startscript (alleen proceskeuze).
|
||||
- Betere betrouwbaarheid bij shutdowns (via tini).
|
||||
- Strikte scheiding tussen **lifecycle taken** en **runtime**.
|
||||
- Snellere builds via shared base image.
|
||||
- Uniforme aanpak in zowel Podman/Compose als Kubernetes.
|
||||
|
||||
108
documentation/mobile-ux.md
Normal file
108
documentation/mobile-ux.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# Mobile UX Improvements for EveAI Chat Client
|
||||
|
||||
This document explains the changes we made to improve the mobile user experience and how to verify or tweak them.
|
||||
|
||||
## Summary of Issues Addressed
|
||||
- Unwanted white margins and page scroll on mobile due to default body margin and mixed vh/dvh usage.
|
||||
- Layout jitter and incorrect heights when the browser address bar or the on-screen keyboard appears.
|
||||
- Chat input potentially being obscured by the keyboard.
|
||||
- Differences between DevTools emulation and real devices.
|
||||
|
||||
## Key Changes
|
||||
|
||||
1. CSS base reset and layout fixes
|
||||
- Remove default white margins and prevent double scroll.
|
||||
- Ensure outer containers don’t create conflicting scrollbars.
|
||||
- Respect device safe-area insets (bottom notch, etc.).
|
||||
|
||||
Files:
|
||||
- eveai_chat_client/templates/base.html
|
||||
- Added a CSS custom property fallback: `--vvh: 1vh;` on `:root`.
|
||||
- eveai_chat_client/static/assets/css/chat.css
|
||||
- `.app-container` now uses `height: calc(var(--vvh, 1vh) * 100);`.
|
||||
- Added base reset: `body { margin: 0; overflow: hidden; }` and global `box-sizing: border-box`.
|
||||
- Mobile `.content-area` no longer uses `calc(100vh - header)`; we rely on flex with `min-height: 0`.
|
||||
- eveai_chat_client/static/assets/css/chat-components.css
|
||||
- Added `padding-bottom: env(safe-area-inset-bottom)` for the chat input area on mobile.
|
||||
- When keyboard is open (detected via JS), we slightly increase bottom padding.
|
||||
|
||||
2. VisualViewport utility and debug overlay
|
||||
- We introduced a small utility that:
|
||||
- Sets a CSS variable `--vvh` equal to 1% of the current visual viewport height.
|
||||
- Toggles a `keyboard-open` class on `<body>` when the keyboard is likely visible.
|
||||
- Optionally shows a debug overlay with real-time viewport metrics.
|
||||
|
||||
Files:
|
||||
- frontend_src/js/utils/viewport.js (new)
|
||||
- Exports `initViewportFix({ enableDebug })`.
|
||||
- Updates `--vvh` on resize/scroll/orientation changes.
|
||||
- Optional debug overlay via `enableDebug`.
|
||||
- frontend_src/js/chat-client.js
|
||||
- Imports and initializes the util on DOMContentLoaded.
|
||||
- Enable overlay via query parameter: `?debug=viewport`.
|
||||
|
||||
## How It Works
|
||||
- CSS reads `--vvh` to size the vertical layout. This follows the visual viewport, so when the URL bar or keyboard affects the viewport, the chat layout adapts without extra page scroll.
|
||||
- The `keyboard-open` body class allows us to slightly increase bottom padding for the input to avoid edge cases where the input is near the keyboard.
|
||||
- On mobile, `.app-container` spans the full visual viewport height, and `.content-area` flexes to take remaining space under the mobile header without relying on height calculations.
|
||||
|
||||
## Testing Checklist
|
||||
1. Open the chat on real devices (iOS Safari, Android Chrome).
|
||||
2. Verify there are no white margins around the app; the overall page should not scroll.
|
||||
3. Focus the message input:
|
||||
- Keyboard appears; the message list should stay usable and the input should remain visible.
|
||||
- The page should not jump in height unexpectedly.
|
||||
4. Rotate device (portrait/landscape) and ensure the layout adapts.
|
||||
5. Try with/without the browser UI (scroll a bit to collapse the address bar on Android Chrome).
|
||||
6. Optional: enable overlay with `?debug=viewport` and verify reported values look sane.
|
||||
|
||||
## Tuning and Fallbacks
|
||||
- If you need to support extremely old iOS versions that lack VisualViewport, we still fall back to `1vh` for `--vvh`. You can increase robustness by setting `--vvh` via a simple `window.innerHeight` measurement on load.
|
||||
- Keyboard detection is heuristic-based (drop > 120px). Adjust the threshold in `viewport.js` if needed for specific device groups.
|
||||
- Avoid adding fixed `height: 100vh` or `calc(100vh - X)` to new containers; prefer flex layouts with `min-height: 0` for scrollable children.
|
||||
|
||||
## Known Limitations
|
||||
- On some embedded browsers (in-app webviews), viewport behavior can deviate; test with your target apps.
|
||||
- The safe-area env variables require modern browsers; most iOS/Android are fine, but always test.
|
||||
|
||||
## Enabling/Disabling Debug Overlay
|
||||
- Append `?debug=viewport` to the URL to enable.
|
||||
- The overlay displays: `innerHeight`, `clientHeight`, `visualViewport.height`, `visualViewport.pageTop`, header height, current `--vvh`, and keyboard state.
|
||||
|
||||
## Next Steps (Optional Enhancements)
|
||||
- PWA manifest with `display: standalone` to reduce address bar effects.
|
||||
- Add smooth scroll-into-view on input focus for edge cases on particular webviews.
|
||||
- Add E2E tests using BrowserStack for a small device matrix.
|
||||
|
||||
## Focus Zoom Prevention (2025-09-24)
|
||||
Some mobile browsers (notably iOS Safari) auto-zoom when focusing inputs with small font sizes. To prevent this:
|
||||
- We enforce font-size: 16px for inputs, selects, textareas, and buttons on mobile in chat-components.css.
|
||||
- We stabilize text scaling via `-webkit-text-size-adjust: 100%`.
|
||||
- We extended the meta viewport with `viewport-fit=cover`.
|
||||
- The debug overlay now also shows `visualViewport.scale` to confirm no zoom is happening (expect 1.00 during focus).
|
||||
|
||||
Files:
|
||||
- eveai_chat_client/static/assets/css/chat-components.css (mobile form control font-size)
|
||||
- eveai_chat_client/static/assets/css/chat.css (text-size-adjust)
|
||||
- eveai_chat_client/templates/base.html (viewport meta)
|
||||
- frontend_src/js/utils/viewport.js (overlay shows vv.scale)
|
||||
|
||||
Validation checklist:
|
||||
- Focus the chat input and language select on iOS Safari and Android Chrome; the UI should not scale up.
|
||||
- Overlay shows `vv.scale: 1.00` with `?debug=viewport`.
|
||||
|
||||
## Mobile Header Overflow Fix (2025-09-24)
|
||||
We observed that the mobile header could push the layout wider than the viewport on small devices, causing horizontal page scroll. We addressed this by:
|
||||
- Allowing the header row to wrap: `.mobile-header { flex-wrap: wrap; }`.
|
||||
- Ensuring children can shrink within the container: `min-width: 0` on `.mobile-logo` and `.mobile-language-selector`.
|
||||
- Constraining controls: `:deep(.language-select)` uses `max-width: 100%` and on very small screens `max-width: 60vw`.
|
||||
- Preventing accidental bleed: `.mobile-header { max-width: 100%; overflow: hidden; }`.
|
||||
- Added `html { overflow-x: hidden; }` as a light global failsafe.
|
||||
|
||||
Files:
|
||||
- eveai_chat_client/static/assets/vue-components/MobileHeader.vue (scoped styles updated)
|
||||
- eveai_chat_client/static/assets/css/chat.css (added html { overflow-x: hidden; })
|
||||
|
||||
Validation checklist:
|
||||
- On 320px width devices, header may wrap to 2 lines without horizontal scroll.
|
||||
- Long tenant names and multiple languages do not cause page-wide overflow; content remains within viewport width.
|
||||
Reference in New Issue
Block a user