- 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:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -55,3 +55,4 @@ scripts/__pycache__/run_eveai_app.cpython-312.pyc
|
|||||||
/nginx/node_modules/
|
/nginx/node_modules/
|
||||||
/nginx/.parcel-cache/
|
/nginx/.parcel-cache/
|
||||||
/nginx/static/
|
/nginx/static/
|
||||||
|
/docker/build_logs/
|
||||||
|
|||||||
45
common/utils/asset_manifest.py
Normal file
45
common/utils/asset_manifest.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
from functools import lru_cache
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
# Default manifest path inside app images; override with env
|
||||||
|
DEFAULT_MANIFEST_PATH = os.environ.get(
|
||||||
|
'EVEAI_STATIC_MANIFEST_PATH',
|
||||||
|
'/app/config/static-manifest/manifest.json'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=1)
|
||||||
|
def _load_manifest(manifest_path: str = DEFAULT_MANIFEST_PATH) -> Dict[str, str]:
|
||||||
|
try:
|
||||||
|
with open(manifest_path, 'r', encoding='utf-8') as f:
|
||||||
|
return json.load(f)
|
||||||
|
except Exception:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_asset(logical_path: str, manifest_path: str = DEFAULT_MANIFEST_PATH) -> str:
|
||||||
|
"""
|
||||||
|
Map a logical asset path (e.g. 'dist/chat-client.js') to the hashed path
|
||||||
|
found in the Parcel manifest. If not found or manifest missing, return the
|
||||||
|
original logical path for graceful fallback.
|
||||||
|
"""
|
||||||
|
if not logical_path:
|
||||||
|
return logical_path
|
||||||
|
|
||||||
|
manifest = _load_manifest(manifest_path)
|
||||||
|
|
||||||
|
# Try several key variants as Parcel manifests may use different keys
|
||||||
|
candidates = [
|
||||||
|
logical_path,
|
||||||
|
logical_path.lstrip('/'),
|
||||||
|
logical_path.replace('static/', ''),
|
||||||
|
logical_path.replace('dist/', ''),
|
||||||
|
]
|
||||||
|
|
||||||
|
for key in candidates:
|
||||||
|
if key in manifest:
|
||||||
|
return manifest[key]
|
||||||
|
|
||||||
|
return logical_path
|
||||||
@@ -107,6 +107,33 @@ def get_pagination_html(pagination, endpoint, **kwargs):
|
|||||||
return Markup(''.join(html))
|
return Markup(''.join(html))
|
||||||
|
|
||||||
|
|
||||||
|
def asset_url(logical_path: str):
|
||||||
|
"""
|
||||||
|
Resolve an asset logical path to a hashed URL using Parcel manifest when available.
|
||||||
|
Fallback to the original logical path under /static/ if manifest is missing.
|
||||||
|
Examples:
|
||||||
|
- asset_url('dist/chat-client.js') -> '/static/dist/chat-client.abc123.js'
|
||||||
|
- asset_url('dist/chat-client.css') -> '/static/dist/chat-client.def456.css'
|
||||||
|
"""
|
||||||
|
if not logical_path:
|
||||||
|
return logical_path
|
||||||
|
try:
|
||||||
|
from common.utils.asset_manifest import resolve_asset
|
||||||
|
resolved = resolve_asset(logical_path)
|
||||||
|
if not resolved:
|
||||||
|
return f"/static/{logical_path.lstrip('/')}"
|
||||||
|
# If resolved is already an absolute URL starting with /static or http(s), return as is
|
||||||
|
if resolved.startswith('/static/') or resolved.startswith('http://') or resolved.startswith('https://'):
|
||||||
|
return resolved
|
||||||
|
# If it starts with 'dist/', prefix /static/
|
||||||
|
if resolved.startswith('dist/'):
|
||||||
|
return '/static/' + resolved
|
||||||
|
# Otherwise, best effort: ensure it lives under /static/
|
||||||
|
return '/static/' + resolved.lstrip('/')
|
||||||
|
except Exception:
|
||||||
|
return f"/static/{logical_path.lstrip('/')}"
|
||||||
|
|
||||||
|
|
||||||
def register_filters(app):
|
def register_filters(app):
|
||||||
"""
|
"""
|
||||||
Registers custom filters with the Flask app.
|
Registers custom filters with the Flask app.
|
||||||
@@ -123,4 +150,5 @@ def register_filters(app):
|
|||||||
app.jinja_env.globals['prefixed_url_for'] = prefixed_url_for
|
app.jinja_env.globals['prefixed_url_for'] = prefixed_url_for
|
||||||
app.jinja_env.globals['get_pagination_html'] = get_pagination_html
|
app.jinja_env.globals['get_pagination_html'] = get_pagination_html
|
||||||
app.jinja_env.globals['get_base_background_color'] = get_base_background_color
|
app.jinja_env.globals['get_base_background_color'] = get_base_background_color
|
||||||
|
app.jinja_env.globals['asset_url'] = asset_url
|
||||||
|
|
||||||
|
|||||||
6
config/static-manifest/manifest.json
Normal file
6
config/static-manifest/manifest.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"dist/chat-client.js": "dist/chat-client.25888758.js",
|
||||||
|
"dist/chat-client.css": "dist/chat-client.eef0ef31.css",
|
||||||
|
"dist/main.js": "dist/main.f3dde0f6.js",
|
||||||
|
"dist/main.css": "dist/main.c40e57ad.css"
|
||||||
|
}
|
||||||
@@ -1,7 +1,49 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# Exit on any error
|
# Safer bash: we manage errors manually (no -e) but detect pipeline failures
|
||||||
set -e
|
set -o pipefail
|
||||||
|
|
||||||
|
# Quiet mode default; enable verbose with --verbose
|
||||||
|
QUIET=${QUIET:-true}
|
||||||
|
|
||||||
|
# Parse --verbose early (we'll reparse fully later as well)
|
||||||
|
for arg in "$@"; do
|
||||||
|
if [[ "$arg" == "--verbose" ]]; then QUIET=false; fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Per-run logs directory
|
||||||
|
RUN_TS=$(date +%Y%m%d_%H%M%S)
|
||||||
|
LOG_DIR="./build_logs/$RUN_TS"
|
||||||
|
mkdir -p "$LOG_DIR"
|
||||||
|
|
||||||
|
# Error aggregation
|
||||||
|
ERRORS=()
|
||||||
|
ERROR_LINES=()
|
||||||
|
EXIT_CODE=0
|
||||||
|
|
||||||
|
# Helper: run_quiet SERVICE STEP -- CMD ARGS...
|
||||||
|
run_quiet() {
|
||||||
|
local SERVICE="$1"; shift
|
||||||
|
local STEP="$1"; shift
|
||||||
|
# Expect a literal "--" separator before the command
|
||||||
|
if [[ "$1" == "--" ]]; then shift; fi
|
||||||
|
local LOG_FILE="$LOG_DIR/${SERVICE}.${STEP}.log"
|
||||||
|
if [[ "$QUIET" == "true" ]]; then
|
||||||
|
"$@" > /dev/null 2> >(tee -a "$LOG_FILE" >&2)
|
||||||
|
else
|
||||||
|
"$@" > >(tee -a "$LOG_FILE") 2> >(tee -a "$LOG_FILE" >&2)
|
||||||
|
fi
|
||||||
|
local RC=$?
|
||||||
|
echo "$LOG_FILE" > "$LOG_DIR/.last_${SERVICE}_${STEP}.path"
|
||||||
|
return $RC
|
||||||
|
}
|
||||||
|
|
||||||
|
record_error() {
|
||||||
|
local SERVICE="$1"; local STEP="$2"; local MESSAGE="$3"; local LOG_FILE="$4"
|
||||||
|
ERRORS+=("$SERVICE|$STEP|$LOG_FILE|$MESSAGE")
|
||||||
|
ERROR_LINES+=("$MESSAGE")
|
||||||
|
EXIT_CODE=1
|
||||||
|
}
|
||||||
|
|
||||||
source ./podman_env_switch.sh dev
|
source ./podman_env_switch.sh dev
|
||||||
|
|
||||||
@@ -39,7 +81,7 @@ BASE_ONLY=""
|
|||||||
|
|
||||||
# Function to display usage information
|
# Function to display usage information
|
||||||
usage() {
|
usage() {
|
||||||
echo "Usage: $0 [-b|-p|-bb|--base-only] [--no-cache] [--progress=plain] [--debug] [service1 service2 ...]"
|
echo "Usage: $0 [-b|-p|-bb|--base-only] [--no-cache] [--progress=plain] [--debug] [--verbose] [service1 service2 ...]"
|
||||||
echo " -b: Build only"
|
echo " -b: Build only"
|
||||||
echo " -p: Push only"
|
echo " -p: Push only"
|
||||||
echo " -bb: Build base image (in addition to services)"
|
echo " -bb: Build base image (in addition to services)"
|
||||||
@@ -47,6 +89,7 @@ usage() {
|
|||||||
echo " --no-cache: Perform a clean build without using cache"
|
echo " --no-cache: Perform a clean build without using cache"
|
||||||
echo " --progress=plain: Show detailed progress of the build"
|
echo " --progress=plain: Show detailed progress of the build"
|
||||||
echo " --debug: Enable debug mode for the build"
|
echo " --debug: Enable debug mode for the build"
|
||||||
|
echo " --verbose: Show full output of build/push (default is quiet; logs always saved under ./build_logs/<timestamp>)"
|
||||||
echo " If no option is provided, both build and push will be performed."
|
echo " If no option is provided, both build and push will be performed."
|
||||||
echo " If no services are specified, all eveai_ services and nginx will be processed."
|
echo " If no services are specified, all eveai_ services and nginx will be processed."
|
||||||
echo " All images are built for AMD64 platform (compatible with both x86_64 and Apple Silicon via emulation)."
|
echo " All images are built for AMD64 platform (compatible with both x86_64 and Apple Silicon via emulation)."
|
||||||
@@ -55,6 +98,10 @@ usage() {
|
|||||||
# Parse command-line options
|
# Parse command-line options
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case $1 in
|
case $1 in
|
||||||
|
--verbose)
|
||||||
|
QUIET=false
|
||||||
|
shift
|
||||||
|
;;
|
||||||
-b)
|
-b)
|
||||||
ACTION="build"
|
ACTION="build"
|
||||||
shift
|
shift
|
||||||
@@ -96,13 +143,13 @@ done
|
|||||||
|
|
||||||
# Function to build base image
|
# Function to build base image
|
||||||
build_base_image() {
|
build_base_image() {
|
||||||
echo "🏗️ Building base image..."
|
echo "🏗️ Building base image... =============================================================="
|
||||||
local BASE_IMAGE_NAME="$REGISTRY/$ACCOUNT/eveai-base:$TAG"
|
local BASE_IMAGE_NAME="$REGISTRY/$ACCOUNT/eveai-base:$TAG"
|
||||||
|
|
||||||
echo "Building base image for platform: $PLATFORM"
|
echo "Building base image for platform: $PLATFORM"
|
||||||
echo "Base image tag: $BASE_IMAGE_NAME"
|
echo "Base image tag: $BASE_IMAGE_NAME"
|
||||||
|
|
||||||
podman build \
|
run_quiet base build -- podman build \
|
||||||
--platform "$PLATFORM" \
|
--platform "$PLATFORM" \
|
||||||
$NO_CACHE \
|
$NO_CACHE \
|
||||||
$PROGRESS \
|
$PROGRESS \
|
||||||
@@ -111,10 +158,22 @@ build_base_image() {
|
|||||||
-t "$BASE_IMAGE_NAME" \
|
-t "$BASE_IMAGE_NAME" \
|
||||||
-f Dockerfile.base \
|
-f Dockerfile.base \
|
||||||
..
|
..
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
LOG_FILE="$LOG_DIR/base.build.log"
|
||||||
|
echo "❌ Failed to build base image"
|
||||||
|
record_error base build "❌ Failed to build base image" "$LOG_FILE"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
if [ "$ACTION" = "push" ] || [ "$ACTION" = "both" ]; then
|
if [ "$ACTION" = "push" ] || [ "$ACTION" = "both" ]; then
|
||||||
echo "Pushing base image to registry..."
|
echo "Pushing base image to registry..."
|
||||||
podman push "$BASE_IMAGE_NAME"
|
run_quiet base push -- podman push "$BASE_IMAGE_NAME"
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
LOG_FILE="$LOG_DIR/base.push.log"
|
||||||
|
echo "❌ Failed to push base image"
|
||||||
|
record_error base push "❌ Failed to push base image" "$LOG_FILE"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "✅ Base image built successfully"
|
echo "✅ Base image built successfully"
|
||||||
@@ -132,7 +191,7 @@ should_build_base() {
|
|||||||
# Function to build and/or push a service
|
# Function to build and/or push a service
|
||||||
process_service() {
|
process_service() {
|
||||||
local SERVICE="$1"
|
local SERVICE="$1"
|
||||||
echo "Processing $SERVICE..."
|
echo "Processing $SERVICE... =================================================================="
|
||||||
|
|
||||||
# Extract the build context and dockerfile from the compose file
|
# Extract the build context and dockerfile from the compose file
|
||||||
CONTEXT=$(yq e ".services.$SERVICE.build.context" compose_dev.yaml)
|
CONTEXT=$(yq e ".services.$SERVICE.build.context" compose_dev.yaml)
|
||||||
@@ -160,8 +219,8 @@ process_service() {
|
|||||||
|
|
||||||
# Build and/or push based on ACTION
|
# Build and/or push based on ACTION
|
||||||
if [ "$ACTION" = "build" ]; then
|
if [ "$ACTION" = "build" ]; then
|
||||||
echo "Building $SERVICE for $PLATFORM..."
|
echo "🛠️ Building $SERVICE for $PLATFORM..."
|
||||||
podman build \
|
run_quiet "$SERVICE" build -- podman build \
|
||||||
--platform "$PLATFORM" \
|
--platform "$PLATFORM" \
|
||||||
$NO_CACHE \
|
$NO_CACHE \
|
||||||
$PROGRESS \
|
$PROGRESS \
|
||||||
@@ -170,26 +229,27 @@ process_service() {
|
|||||||
-t "$REGISTRY_IMAGE_NAME" \
|
-t "$REGISTRY_IMAGE_NAME" \
|
||||||
-f "$CONTEXT/$DOCKERFILE" \
|
-f "$CONTEXT/$DOCKERFILE" \
|
||||||
"$CONTEXT"
|
"$CONTEXT"
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
LOG_FILE="$LOG_DIR/${SERVICE}.build.log"
|
||||||
|
echo "❌ Failed to build $SERVICE"
|
||||||
|
record_error "$SERVICE" build "❌ Failed to build $SERVICE" "$LOG_FILE"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
elif [ "$ACTION" = "push" ]; then
|
elif [ "$ACTION" = "push" ]; then
|
||||||
echo "Building and pushing $SERVICE for $PLATFORM..."
|
echo "📤 Pushing $SERVICE to registry..."
|
||||||
podman build \
|
run_quiet "$SERVICE" push -- podman push "$REGISTRY_IMAGE_NAME"
|
||||||
--platform "$PLATFORM" \
|
if [ $? -ne 0 ]; then
|
||||||
$NO_CACHE \
|
LOG_FILE="$LOG_DIR/${SERVICE}.push.log"
|
||||||
$PROGRESS \
|
echo "❌ Failed to push $SERVICE"
|
||||||
$DEBUG \
|
record_error "$SERVICE" push "❌ Failed to push $SERVICE" "$LOG_FILE"
|
||||||
-t "$LOCAL_IMAGE_NAME" \
|
return 1
|
||||||
-t "$REGISTRY_IMAGE_NAME" \
|
fi
|
||||||
-f "$CONTEXT/$DOCKERFILE" \
|
|
||||||
"$CONTEXT"
|
|
||||||
|
|
||||||
echo "Pushing $SERVICE to registry..."
|
|
||||||
podman push "$REGISTRY_IMAGE_NAME"
|
|
||||||
|
|
||||||
else
|
else
|
||||||
# Both build and push
|
# Both build and push
|
||||||
echo "Building $SERVICE for $PLATFORM..."
|
echo "🛠️ Building $SERVICE for $PLATFORM..."
|
||||||
podman build \
|
run_quiet "$SERVICE" build -- podman build \
|
||||||
--platform "$PLATFORM" \
|
--platform "$PLATFORM" \
|
||||||
$NO_CACHE \
|
$NO_CACHE \
|
||||||
$PROGRESS \
|
$PROGRESS \
|
||||||
@@ -198,9 +258,21 @@ process_service() {
|
|||||||
-t "$REGISTRY_IMAGE_NAME" \
|
-t "$REGISTRY_IMAGE_NAME" \
|
||||||
-f "$CONTEXT/$DOCKERFILE" \
|
-f "$CONTEXT/$DOCKERFILE" \
|
||||||
"$CONTEXT"
|
"$CONTEXT"
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
LOG_FILE="$LOG_DIR/${SERVICE}.build.log"
|
||||||
|
echo "❌ Failed to build $SERVICE"
|
||||||
|
record_error "$SERVICE" build "❌ Failed to build $SERVICE" "$LOG_FILE"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
echo "Pushing $SERVICE to registry..."
|
echo "📤 Pushing $SERVICE to registry..."
|
||||||
podman push "$REGISTRY_IMAGE_NAME"
|
run_quiet "$SERVICE" push -- podman push "$REGISTRY_IMAGE_NAME"
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
LOG_FILE="$LOG_DIR/${SERVICE}.push.log"
|
||||||
|
echo "❌ Failed to push $SERVICE"
|
||||||
|
record_error "$SERVICE" push "❌ Failed to push $SERVICE" "$LOG_FILE"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,22 +303,87 @@ fi
|
|||||||
echo "Using simplified AMD64-only approach for maximum compatibility..."
|
echo "Using simplified AMD64-only approach for maximum compatibility..."
|
||||||
echo "Images will be tagged as: $REGISTRY/$ACCOUNT/[service]:$TAG"
|
echo "Images will be tagged as: $REGISTRY/$ACCOUNT/[service]:$TAG"
|
||||||
|
|
||||||
|
# Reorder to ensure nginx builds before eveai_* if both are present
|
||||||
|
HAS_NGINX=false
|
||||||
|
HAS_APPS=false
|
||||||
|
for S in "${SERVICES[@]}"; do
|
||||||
|
if [[ "$S" == "nginx" ]]; then HAS_NGINX=true; fi
|
||||||
|
if [[ "$S" == eveai_* ]]; then HAS_APPS=true; fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if $HAS_NGINX && $HAS_APPS; then
|
||||||
|
ORDERED_SERVICES=("nginx")
|
||||||
|
for S in "${SERVICES[@]}"; do
|
||||||
|
if [[ "$S" != "nginx" ]]; then ORDERED_SERVICES+=("$S"); fi
|
||||||
|
done
|
||||||
|
SERVICES=("${ORDERED_SERVICES[@]}")
|
||||||
|
fi
|
||||||
|
|
||||||
# Loop through services
|
# Loop through services
|
||||||
for SERVICE in "${SERVICES[@]}"; do
|
for SERVICE in "${SERVICES[@]}"; do
|
||||||
if [[ "$SERVICE" == "nginx" ]]; then
|
if [[ "$SERVICE" == "nginx" ]]; then
|
||||||
./copy_specialist_svgs.sh ../config ../nginx/static/assets 2>/dev/null || echo "Warning: copy_specialist_svgs.sh not found or failed"
|
run_quiet nginx copy-specialist-svgs -- ./copy_specialist_svgs.sh ../config ../nginx/static/assets
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
LOG_FILE="$LOG_DIR/nginx.copy-specialist-svgs.log"
|
||||||
|
echo "⚠️ copy_specialist_svgs.sh not found or failed"
|
||||||
|
record_error nginx copy-specialist-svgs "⚠️ copy_specialist_svgs.sh not found or failed" "$LOG_FILE"
|
||||||
fi
|
fi
|
||||||
|
run_quiet nginx rebuild-chat-client -- ./rebuild_chat_client.sh
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
LOG_FILE="$LOG_DIR/nginx.rebuild-chat-client.log"
|
||||||
|
echo "❌ rebuild_chat_client.sh failed"
|
||||||
|
record_error nginx rebuild-chat-client "❌ rebuild_chat_client.sh failed" "$LOG_FILE"
|
||||||
|
fi
|
||||||
|
MANIFEST_SRC="../nginx/static/dist/manifest.json"
|
||||||
|
MANIFEST_DST_DIR="../config/static-manifest"
|
||||||
|
MANIFEST_DST="$MANIFEST_DST_DIR/manifest.json"
|
||||||
|
if [ ! -f "$MANIFEST_SRC" ]; then
|
||||||
|
if $HAS_NGINX; then
|
||||||
|
echo "⚠️ manifest.json not found at $MANIFEST_SRC yet. nginx should be built first in this run."
|
||||||
|
else
|
||||||
|
echo "❌ manifest.json not found at $MANIFEST_SRC. Please build nginx (assets) first."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
mkdir -p "$MANIFEST_DST_DIR"
|
||||||
|
if [ -f "$MANIFEST_SRC" ]; then
|
||||||
|
cp -f "$MANIFEST_SRC" "$MANIFEST_DST"
|
||||||
|
echo "📄 Staged manifest at $MANIFEST_DST"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ "$SERVICE" == "nginx" || "$SERVICE" == eveai_* || "$SERVICE" == "prometheus" || "$SERVICE" == "grafana" ]]; then
|
if [[ "$SERVICE" == "nginx" || "$SERVICE" == eveai_* || "$SERVICE" == "prometheus" || "$SERVICE" == "grafana" ]]; then
|
||||||
if process_service "$SERVICE"; then
|
if process_service "$SERVICE"; then
|
||||||
echo "✅ Successfully processed $SERVICE"
|
echo "✅ Successfully processed $SERVICE"
|
||||||
else
|
else
|
||||||
echo "❌ Failed to process $SERVICE"
|
echo "❌ Failed to process $SERVICE"
|
||||||
|
ERROR_LINES+=("❌ Failed to process $SERVICE")
|
||||||
|
EXIT_CODE=1
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo "⏭️ Skipping $SERVICE as it's not nginx, prometheus, grafana or doesn't start with eveai_"
|
echo "⏭️ Skipping $SERVICE as it's not nginx, prometheus, grafana or doesn't start with eveai_"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
echo -e "\033[32m✅ All specified services processed successfully!\033[0m"
|
if [ ${#ERRORS[@]} -eq 0 ]; then
|
||||||
echo -e "\033[32m📦 Images are available locally and in registry\033[0m"
|
echo -e "\033[32m✅ All specified services processed successfully!\033[0m"
|
||||||
|
echo -e "\033[32m📦 Images are available locally and in registry\033[0m"
|
||||||
|
else
|
||||||
|
echo -e "\033[31m❌ One or more errors occurred during build/push\033[0m"
|
||||||
|
# Reprint short failure lines (your concise messages)
|
||||||
|
for LINE in "${ERROR_LINES[@]}"; do
|
||||||
|
echo "$LINE"
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
echo "Details (see logs for full output):"
|
||||||
|
for ITEM in "${ERRORS[@]}"; do
|
||||||
|
SERVICE_STEP_MSG_LOG=$(echo "$ITEM")
|
||||||
|
IFS='|' read -r SVC STEP LOGFILE MSG <<< "$SERVICE_STEP_MSG_LOG"
|
||||||
|
echo "- Service: $SVC | Step: $STEP"
|
||||||
|
echo " ↳ Log: $LOGFILE"
|
||||||
|
done
|
||||||
|
EXIT_CODE=1
|
||||||
|
fi
|
||||||
|
# Always print finished timestamp
|
||||||
echo -e "\033[32m🕐 Finished at $(date +"%d/%m/%Y %H:%M:%S")\033[0m"
|
echo -e "\033[32m🕐 Finished at $(date +"%d/%m/%Y %H:%M:%S")\033[0m"
|
||||||
|
exit $EXIT_CODE
|
||||||
@@ -3,4 +3,3 @@ FROM registry.ask-eve-ai-local.com/josakola/eveai-base:latest
|
|||||||
# Copy the service-specific source code into the container.
|
# Copy the service-specific source code into the container.
|
||||||
COPY eveai_chat_client /app/eveai_chat_client
|
COPY eveai_chat_client /app/eveai_chat_client
|
||||||
COPY content /app/content
|
COPY content /app/content
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
# Use the official Nginx image as the base image
|
# Use the official Nginx image as the base image
|
||||||
FROM nginx:latest
|
ARG TARGETPLATFORM
|
||||||
|
FROM --platform=$TARGETPLATFORM nginx:latest
|
||||||
|
|
||||||
|
# Ensure we use user root
|
||||||
|
USER root
|
||||||
# Copy the custom Nginx configuration file into the container
|
# Copy the custom Nginx configuration file into the container
|
||||||
COPY ../../nginx/nginx.conf /etc/nginx/nginx.conf
|
COPY ../../nginx/nginx.conf /etc/nginx/nginx.conf
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,12 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
cd /Volumes/OWC4M2_1/Development/Josako/EveAI/TBD/docker
|
|
||||||
source ./docker_env_switch.sh dev
|
|
||||||
|
|
||||||
echo "Copying client images"
|
echo "Copying client images"
|
||||||
cp -fv ../eveai_chat_client/static/assets/img/* ../nginx/static/assets/img
|
cp -fv ../eveai_chat_client/static/assets/img/* ../nginx/static/assets/img
|
||||||
|
|
||||||
|
|
||||||
pcdown
|
|
||||||
|
|
||||||
cd ../nginx
|
cd ../nginx
|
||||||
|
|
||||||
npm run clean
|
npm run clean
|
||||||
npm run build
|
npm run build
|
||||||
|
|
||||||
cd ../docker
|
cd ../docker/
|
||||||
./build_and_push_eveai.sh -b nginx
|
|
||||||
|
|
||||||
pcup
|
|
||||||
|
|||||||
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.
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css">
|
||||||
|
|
||||||
<!-- Gebundelde CSS (bevat nu al je CSS) -->
|
<!-- Gebundelde CSS (bevat nu al je CSS) -->
|
||||||
<link href="{{url_for('static', filename='dist/main.css')}}" rel="stylesheet" />
|
<link href="{{ asset_url('dist/main.css') }}" rel="stylesheet" />
|
||||||
<base href="/admin/">
|
<base href="/admin/">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{#dist/main.js contains all used javascript libraries#}
|
{#dist/main.js contains all used javascript libraries#}
|
||||||
<script src="{{url_for('static', filename='dist/main.js')}}"></script>
|
<script src="{{ asset_url('dist/main.js') }}"></script>
|
||||||
|
|
||||||
{% include 'eveai_json_editor.html' %}
|
{% include 'eveai_json_editor.html' %}
|
||||||
{% include 'eveai_ordered_list_editor.html' %}
|
{% include 'eveai_ordered_list_editor.html' %}
|
||||||
|
|||||||
@@ -79,6 +79,16 @@
|
|||||||
|
|
||||||
.chat-input-area {
|
.chat-input-area {
|
||||||
max-width: 100%; /* Op mobiel volledige breedte gebruiken */
|
max-width: 100%; /* Op mobiel volledige breedte gebruiken */
|
||||||
|
/* Respect safe-area at bottom */
|
||||||
|
padding-bottom: env(safe-area-inset-bottom, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Prevent iOS focus zoom by ensuring form controls are >= 16px */
|
||||||
|
input, textarea, select, button,
|
||||||
|
.chat-input input, .chat-input textarea, .chat-input select, .chat-input button,
|
||||||
|
.mobile-language-selector :is(select, input, button) {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-input {
|
.chat-input {
|
||||||
@@ -97,6 +107,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* When keyboard is open, allow a bit more space below */
|
||||||
|
body.keyboard-open .chat-input-area {
|
||||||
|
padding-bottom: calc(env(safe-area-inset-bottom, 0) + 8px);
|
||||||
|
}
|
||||||
|
|
||||||
/* Extra small screens */
|
/* Extra small screens */
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
.chat-app-container {
|
.chat-app-container {
|
||||||
@@ -251,15 +266,6 @@
|
|||||||
margin: 20px 0;
|
margin: 20px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-message .message-content {
|
|
||||||
max-width: 90%;
|
|
||||||
background: white;
|
|
||||||
border: 1px solid #e9ecef;
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 20px;
|
|
||||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* System messages */
|
/* System messages */
|
||||||
.system-message {
|
.system-message {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -437,24 +443,6 @@
|
|||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Mobile responsiveness */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.message {
|
|
||||||
padding: 0 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* moved to ChatMessage.vue scoped styles */
|
|
||||||
|
|
||||||
.message.user .message-content {
|
|
||||||
margin-left: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message.ai .message-content,
|
|
||||||
.message.bot .message-content {
|
|
||||||
margin-right: 40px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
.message {
|
.message {
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
|
|||||||
@@ -63,13 +63,6 @@
|
|||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dynamic form container transitions */
|
|
||||||
.dynamic-form-container {
|
|
||||||
transition: opacity 0.3s ease-in-out, transform 0.3s ease-in-out;
|
|
||||||
transform: translateY(0);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Chat input transitions */
|
/* Chat input transitions */
|
||||||
.chat-input {
|
.chat-input {
|
||||||
transition: opacity 0.3s ease-in-out, transform 0.3s ease-in-out;
|
transition: opacity 0.3s ease-in-out, transform 0.3s ease-in-out;
|
||||||
|
|||||||
@@ -146,11 +146,6 @@
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ensure forms in messages use full available width */
|
|
||||||
.message .dynamic-form-container {
|
|
||||||
width: 100%;
|
|
||||||
max-width: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message .dynamic-form {
|
.message .dynamic-form {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -14,8 +14,9 @@
|
|||||||
/* App container layout */
|
/* App container layout */
|
||||||
.app-container {
|
.app-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 100vh; /* fallback */
|
/* Use visual viewport variable when available */
|
||||||
height: 100dvh; /* prefer dynamic viewport unit */
|
min-height: 0;
|
||||||
|
height: calc(var(--vvh, 1vh) * 100);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,8 +86,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
height: 100vh; /* fallback for desktop */
|
height: auto; /* prefer dynamic viewport on desktop */
|
||||||
height: 100dvh; /* prefer dynamic viewport on desktop */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-container {
|
.chat-container {
|
||||||
@@ -97,16 +97,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
html, body {
|
html, body {
|
||||||
height: 100%;
|
height: calc(var(--vvh, 1vh) * 100); min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Base reset & overflow control */
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
html { overflow-x: hidden; -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }
|
||||||
body {
|
body {
|
||||||
|
margin: 0;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
background-color: var(--background-color);
|
background-color: var(--background-color);
|
||||||
height: 100vh;
|
overflow: hidden; /* prevent double scroll; inner containers handle scrolling */
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
@@ -281,14 +284,6 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root { --mobile-header-height: 60px; } /* default/minimum */
|
:root { --mobile-header-height: 60px; } /* default/minimum */
|
||||||
|
|
||||||
/* Content area takes remaining space */
|
|
||||||
.content-area {
|
|
||||||
flex: 1;
|
|
||||||
height: calc(100vh - var(--mobile-header-height)); /* fallback */
|
|
||||||
height: calc(100dvh - var(--mobile-header-height)); /* prefer dynamic viewport */
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsieve design regels worden nu gedefinieerd in chat-components.css */
|
/* Responsieve design regels worden nu gedefinieerd in chat-components.css */
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
/* Dynamisch formulier stijlen */
|
/* Dynamisch formulier stijlen */
|
||||||
|
|
||||||
.dynamic-form {
|
|
||||||
padding: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-header {
|
.form-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -434,13 +434,13 @@ export default {
|
|||||||
.chat-input-container {
|
.chat-input-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 1000px;
|
max-width: 1000px;
|
||||||
padding: 20px;
|
padding: 10px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background-color: var(--active-background-color);
|
background-color: var(--active-background-color);
|
||||||
color: var(--human-message-text-color);
|
color: var(--human-message-text-color);
|
||||||
border-top: 1px solid #e0e0e0;
|
border-top: 1px solid #e0e0e0;
|
||||||
font-family: Arial, sans-serif;
|
font-family: Arial, sans-serif;
|
||||||
font-size: 14px;
|
font-size: 16px;
|
||||||
transition: opacity 0.2s ease-in-out;
|
transition: opacity 0.2s ease-in-out;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
@@ -470,7 +470,7 @@ export default {
|
|||||||
outline: none;
|
outline: none;
|
||||||
transition: border-color 0.2s;
|
transition: border-color 0.2s;
|
||||||
font-family: Arial, sans-serif;
|
font-family: Arial, sans-serif;
|
||||||
font-size: 14px;
|
font-size: 16px;
|
||||||
/* Transparante achtergrond in plaats van wit */
|
/* Transparante achtergrond in plaats van wit */
|
||||||
background-color: var(--human-message-background);
|
background-color: var(--human-message-background);
|
||||||
color: var(--human-message-text-color);
|
color: var(--human-message-text-color);
|
||||||
@@ -534,14 +534,14 @@ export default {
|
|||||||
|
|
||||||
/* Active AI Message Area - positioned at top of ChatInput */
|
/* Active AI Message Area - positioned at top of ChatInput */
|
||||||
.active-ai-message-area {
|
.active-ai-message-area {
|
||||||
margin-bottom: 15px;
|
margin-bottom: 10px;
|
||||||
padding: 12px;
|
|
||||||
background-color: var(--ai-message-background);
|
background-color: var(--ai-message-background);
|
||||||
color: var(--ai-message-text-color);
|
color: var(--ai-message-text-color);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
font-family: Arial, sans-serif;
|
font-family: Arial, sans-serif;
|
||||||
font-size: 14px;
|
font-size: 16px;
|
||||||
box-shadow: 0 2px 15px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
padding: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ensure the active AI message integrates well with ChatInput styling */
|
/* Ensure the active AI message integrates well with ChatInput styling */
|
||||||
|
|||||||
@@ -432,7 +432,7 @@ export default {
|
|||||||
.message-content {
|
.message-content {
|
||||||
font-family: Arial, sans-serif;
|
font-family: Arial, sans-serif;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
max-width: 90%;
|
max-width: 100%;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
@@ -663,8 +663,7 @@ export default {
|
|||||||
.message.bot .message-content {
|
.message.bot .message-content {
|
||||||
background: var(--ai-message-background);
|
background: var(--ai-message-background);
|
||||||
color: var(--ai-message-text-color);
|
color: var(--ai-message-text-color);
|
||||||
border-bottom-left-radius: 4px;
|
margin-right: 20px;
|
||||||
margin-right: 60px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hover effects for message bubbles */
|
/* Hover effects for message bubbles */
|
||||||
@@ -692,27 +691,18 @@ export default {
|
|||||||
}
|
}
|
||||||
.message.ai .message-content,
|
.message.ai .message-content,
|
||||||
.message.bot .message-content {
|
.message.bot .message-content {
|
||||||
margin-right: 40px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
/* Mobile: place logo inside bubble and prevent overlap with text */
|
/* Mobile: place logo inside bubble and prevent overlap with text */
|
||||||
.message.ai .ai-message-logo {
|
.message.ai .ai-message-logo {
|
||||||
top: -12px;
|
top: -12px;
|
||||||
left: 8px;
|
|
||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
}
|
}
|
||||||
.message.ai .message-content {
|
.message.ai .message-content {
|
||||||
/* Reserve space for the in-bubble logo */
|
/* Reserve space for the in-bubble logo */
|
||||||
padding-top: 1px; /* 24px logo + margins */
|
padding-top: 1px; /* 24px logo + margins */
|
||||||
padding-left: 1px;
|
padding-left: 10px;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.message-content {
|
|
||||||
max-width: 90%;
|
|
||||||
margin-left: 20px !important;
|
|
||||||
margin-right: 20px !important;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -538,14 +538,20 @@ export default {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
/* Dynamisch formulier stijlen */
|
/* Dynamisch formulier stijlen */
|
||||||
.dynamic-form-container {
|
.dynamic-form-container {
|
||||||
margin-bottom: 15px;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
transition: opacity 0.3s ease-in-out, transform 0.3s ease-in-out;
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message .dynamic-form-container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dynamic-form {
|
.dynamic-form {
|
||||||
background: var(--human-message-background);
|
background: var(--human-message-background);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 15px;
|
|
||||||
box-shadow: 0 2px 15px rgba(0,0,0,0.1);
|
box-shadow: 0 2px 15px rgba(0,0,0,0.1);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
|||||||
@@ -294,7 +294,7 @@ export default {
|
|||||||
border-bottom: 2px solid #000;
|
border-bottom: 2px solid #000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* migrated from global css: message-content within form-message */
|
|
||||||
.form-message .message-content {
|
.form-message .message-content {
|
||||||
max-width: 90%;
|
max-width: 90%;
|
||||||
background: white;
|
background: white;
|
||||||
|
|||||||
@@ -80,21 +80,24 @@ const handleLanguageChange = (newLanguage) => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
flex-wrap: wrap; /* allow wrapping to next line on narrow screens */
|
||||||
padding: 10px 15px;
|
padding: 10px 15px;
|
||||||
background: var(--sidebar-background);
|
background: var(--sidebar-background);
|
||||||
color: var(--sidebar-color);
|
color: var(--sidebar-color);
|
||||||
border-bottom: 1px solid rgba(0,0,0,0.1);
|
border-bottom: 1px solid rgba(0,0,0,0.1);
|
||||||
min-height: 60px;
|
min-height: 60px;
|
||||||
|
max-width: 100%; /* never exceed viewport width */
|
||||||
|
overflow: hidden; /* clip any accidental overflow */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Mobile logo container - meer specifieke styling */
|
/* Mobile logo container - meer specifieke styling */
|
||||||
.mobile-logo {
|
.mobile-logo {
|
||||||
flex-shrink: 0;
|
flex-shrink: 1; /* allow logo area to shrink */
|
||||||
|
min-width: 0; /* allow shrinking below intrinsic width */
|
||||||
display: flex !important;
|
display: flex !important;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
height: 50px; /* Vaste hoogte voor consistentie */
|
height: 50px; /* Vaste hoogte voor consistentie */
|
||||||
min-width: 120px; /* Minimale breedte */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Diepere styling voor het logo component */
|
/* Diepere styling voor het logo component */
|
||||||
@@ -105,6 +108,7 @@ const handleLanguageChange = (newLanguage) => {
|
|||||||
align-items: center !important;
|
align-items: center !important;
|
||||||
justify-content: flex-start !important;
|
justify-content: flex-start !important;
|
||||||
height: 100% !important;
|
height: 100% !important;
|
||||||
|
min-width: 0 !important; /* allow inner content to shrink */
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-logo :deep(.logo-image) {
|
.mobile-logo :deep(.logo-image) {
|
||||||
@@ -128,7 +132,7 @@ const handleLanguageChange = (newLanguage) => {
|
|||||||
/* Mobile language selector styling */
|
/* Mobile language selector styling */
|
||||||
.mobile-language-selector {
|
.mobile-language-selector {
|
||||||
flex-shrink: 1;
|
flex-shrink: 1;
|
||||||
min-width: 140px;
|
min-width: 0; /* allow selector area to shrink */
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-language-selector :deep(.language-selector) {
|
.mobile-language-selector :deep(.language-selector) {
|
||||||
@@ -142,10 +146,18 @@ const handleLanguageChange = (newLanguage) => {
|
|||||||
.mobile-language-selector :deep(.language-select) {
|
.mobile-language-selector :deep(.language-select) {
|
||||||
padding: 6px 10px;
|
padding: 6px 10px;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
min-width: 120px;
|
min-width: 0; /* allow the select to shrink */
|
||||||
|
max-width: 100%; /* never exceed container width */
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Extra constraints on ultra-small screens */
|
||||||
|
@media (max-width: 360px) {
|
||||||
|
.mobile-language-selector :deep(.language-select) {
|
||||||
|
max-width: 60vw; /* avoid pushing beyond viewport */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Media queries voor responsiviteit */
|
/* Media queries voor responsiviteit */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.mobile-header {
|
.mobile-header {
|
||||||
|
|||||||
@@ -2,14 +2,17 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||||
<title>{% block title %}EveAI Chat{% endblock %}</title>
|
<title>{% block title %}EveAI Chat{% endblock %}</title>
|
||||||
|
|
||||||
<link href="{{url_for('static', filename='dist/chat-client.css')}}" rel="stylesheet" />
|
<link href="{{ asset_url('dist/chat-client.css') }}" rel="stylesheet" />
|
||||||
|
|
||||||
<!-- Custom theme colors from tenant settings -->
|
<!-- Custom theme colors from tenant settings -->
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
|
/* Visual viewport vh unit fallback (1vh if JS not yet set) */
|
||||||
|
--vvh: 1vh;
|
||||||
|
|
||||||
/* Legacy support - keeping for backward compatibility only */
|
/* Legacy support - keeping for backward compatibility only */
|
||||||
/* These variables are deprecated and should not be used in new code */
|
/* These variables are deprecated and should not be used in new code */
|
||||||
--primary-color: {{ customisation.active_background_color|default('#ffffff') }};
|
--primary-color: {{ customisation.active_background_color|default('#ffffff') }};
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Chat client JS - bundled met alle componenten en ES modules -->
|
<!-- Chat client JS - bundled met alle componenten en ES modules -->
|
||||||
<script src="{{url_for('static', filename='dist/chat-client.js')}}"></script>
|
<script src="{{ asset_url('dist/chat-client.js') }}"></script>
|
||||||
<script>
|
<script>
|
||||||
// Voeg taalinstellingen toe aan chatConfig indien deze nog niet bestaan
|
// Voeg taalinstellingen toe aan chatConfig indien deze nog niet bestaan
|
||||||
if (window.chatConfig) {
|
if (window.chatConfig) {
|
||||||
|
|||||||
@@ -1,97 +0,0 @@
|
|||||||
// Chat Client JavaScript modules
|
|
||||||
// Modern gebundelde versie met ES modules
|
|
||||||
|
|
||||||
// CSS imports - zorg dat deze bestanden bestaan en correct worden gebundeld door Parcel
|
|
||||||
import '../css/chat-client.css';
|
|
||||||
// CSS imports uit eveai_chat_client
|
|
||||||
import '../../../eveai_chat_client/static/assets/css/chat.css';
|
|
||||||
import '../../../eveai_chat_client/static/assets/css/chat-components.css';
|
|
||||||
import '../../../eveai_chat_client/static/assets/css/chat-input.css';
|
|
||||||
import '../../../eveai_chat_client/static/assets/css/chat-message.css';
|
|
||||||
import '../../../eveai_chat_client/static/assets/css/form.css';
|
|
||||||
import '../../../eveai_chat_client/static/assets/css/form-message.css';
|
|
||||||
import '../../../eveai_chat_client/static/assets/css/language-selector.css';
|
|
||||||
|
|
||||||
// Dependencies
|
|
||||||
import { createApp } from 'vue';
|
|
||||||
import { marked } from 'marked';
|
|
||||||
|
|
||||||
// Maak fundamentele libraries globaal beschikbaar
|
|
||||||
window.Vue = { createApp };
|
|
||||||
window.marked = marked;
|
|
||||||
|
|
||||||
// Gebruik barrel export voor componenten
|
|
||||||
import * as Components from '../../../eveai_chat_client/static/assets/vue-components/index.js';
|
|
||||||
|
|
||||||
// Maak Components globaal beschikbaar voor debugging
|
|
||||||
window.Components = Components;
|
|
||||||
console.log('Components loaded:', Object.keys(Components));
|
|
||||||
|
|
||||||
// Main chat application - moet als laatste worden geladen
|
|
||||||
import ChatApp from '../../../eveai_chat_client/static/assets/vue-components/ChatApp.vue';
|
|
||||||
|
|
||||||
// Wacht tot het document volledig is geladen voordat we Vue initialiseren
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
console.log('Initializing Chat Application');
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Mount SideBar component to sidebar-container
|
|
||||||
const sidebarContainer = document.getElementById('sidebar-container');
|
|
||||||
if (sidebarContainer && Components.SideBar) {
|
|
||||||
const sidebarApp = createApp(Components.SideBar, {
|
|
||||||
tenantMake: window.chatConfig?.tenantMake || { name: 'EveAI', logo_url: '' },
|
|
||||||
explanationText: window.chatConfig?.explanationText || '',
|
|
||||||
initialLanguage: window.chatConfig?.language || 'en',
|
|
||||||
supportedLanguageDetails: window.chatConfig?.supportedLanguageDetails || {},
|
|
||||||
allowedLanguages: window.chatConfig?.allowedLanguages || ['nl', 'en', 'fr', 'de'],
|
|
||||||
apiPrefix: window.chatConfig?.apiPrefix || ''
|
|
||||||
});
|
|
||||||
|
|
||||||
// Registreer componenten voor sidebar app
|
|
||||||
for (const [name, component] of Object.entries(Components)) {
|
|
||||||
sidebarApp.component(name, component);
|
|
||||||
}
|
|
||||||
|
|
||||||
sidebarApp.mount('#sidebar-container');
|
|
||||||
console.log('SideBar mounted successfully');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mount MobileHeader component to mobile-header-container
|
|
||||||
const mobileHeaderContainer = document.getElementById('mobile-header-container');
|
|
||||||
if (mobileHeaderContainer && Components.MobileHeader) {
|
|
||||||
const mobileHeaderApp = createApp(Components.MobileHeader, {
|
|
||||||
tenantMake: window.chatConfig?.tenantMake || { name: 'EveAI', logo_url: '' },
|
|
||||||
initialLanguage: window.chatConfig?.language || 'en',
|
|
||||||
supportedLanguageDetails: window.chatConfig?.supportedLanguageDetails || {},
|
|
||||||
allowedLanguages: window.chatConfig?.allowedLanguages || ['nl', 'en', 'fr', 'de'],
|
|
||||||
apiPrefix: window.chatConfig?.apiPrefix || ''
|
|
||||||
});
|
|
||||||
|
|
||||||
// Registreer componenten voor mobile header app
|
|
||||||
for (const [name, component] of Object.entries(Components)) {
|
|
||||||
mobileHeaderApp.component(name, component);
|
|
||||||
}
|
|
||||||
|
|
||||||
mobileHeaderApp.mount('#mobile-header-container');
|
|
||||||
console.log('MobileHeader mounted successfully');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mount ChatApp to the chat container
|
|
||||||
const chatContainer = document.querySelector('.chat-container');
|
|
||||||
if (chatContainer) {
|
|
||||||
const chatApp = createApp(ChatApp);
|
|
||||||
|
|
||||||
// Registreer alle componenten globaal voor chat app
|
|
||||||
for (const [name, component] of Object.entries(Components)) {
|
|
||||||
chatApp.component(name, component);
|
|
||||||
}
|
|
||||||
|
|
||||||
chatApp.mount('.chat-container');
|
|
||||||
console.log('ChatApp mounted successfully');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('All Vue apps mounted successfully');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error initializing Vue applications:', error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
12
nginx/frontend_src/entries/chat-client.html
Normal file
12
nginx/frontend_src/entries/chat-client.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Chat Client Entry</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Minimal HTML entry used solely for Parcel to produce hashed JS/CSS bundles. -->
|
||||||
|
<script type="module" src="../js/chat-client.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
12
nginx/frontend_src/entries/main.html
Normal file
12
nginx/frontend_src/entries/main.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Main Entry</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Minimal HTML entry used solely for Parcel to produce hashed JS/CSS bundles. -->
|
||||||
|
<script type="module" src="../js/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -62,6 +62,12 @@ http {
|
|||||||
alias /etc/nginx/static/;
|
alias /etc/nginx/static/;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Long-term caching for hashed assets built by Parcel
|
||||||
|
location ^~ /static/dist/ {
|
||||||
|
alias /etc/nginx/static/dist/;
|
||||||
|
add_header Cache-Control "public, max-age=31536000, immutable" always;
|
||||||
|
}
|
||||||
|
|
||||||
#error_page 404 /404.html;
|
#error_page 404 /404.html;
|
||||||
|
|
||||||
# redirect server error pages to the static page /50x.html
|
# redirect server error pages to the static page /50x.html
|
||||||
|
|||||||
1357
nginx/package-lock.json
generated
1357
nginx/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -38,14 +38,25 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"prebuild": "mkdir -p static/dist && npm run sync-assets",
|
"prebuild": "mkdir -p static/dist && npm run sync-assets",
|
||||||
"sync-assets": "rsync -av ../eveai_app/static/assets/ static/assets/ && rsync -av ../eveai_chat_client/static/assets/ static/assets/",
|
"sync-assets": "rsync -av ../eveai_app/static/assets/ static/assets/ && rsync -av ../eveai_chat_client/static/assets/ static/assets/",
|
||||||
"build": "npm run prebuild && npm run build:main && npm run build:chat",
|
"build": "npm run prebuild && npm run build:main && npm run build:chat && npm run postbuild",
|
||||||
"build:main": "parcel build frontend_src/js/main.js --dist-dir static/dist --public-url /static/dist/ --no-source-maps",
|
"build:main": "NODE_ENV=production parcel build frontend_src/entries/main.html",
|
||||||
"build:chat": "parcel build frontend_src/js/chat-client.js --dist-dir static/dist --public-url /static/dist/ --no-source-maps",
|
"build:chat": "NODE_ENV=production parcel build frontend_src/entries/chat-client.html",
|
||||||
"predev": "mkdir -p static/dist && npm run sync-assets",
|
"predev": "mkdir -p static/dist && npm run sync-assets",
|
||||||
"dev": "npm run predev && parcel frontend_src/js/main.js --dist-dir static/dist --public-url /static/dist/ & parcel frontend_src/js/chat-client.js --dist-dir static/dist --public-url /static/dist/",
|
"dev": "npm run predev && parcel frontend_src/js/main.js --dist-dir static/dist --public-url /static/dist/ & parcel frontend_src/js/chat-client.js --dist-dir static/dist --public-url /static/dist/",
|
||||||
"prewatch": "mkdir -p static/dist && npm run sync-assets",
|
"prewatch": "mkdir -p static/dist && npm run sync-assets",
|
||||||
"watch": "npm run prewatch && parcel watch frontend_src/js/main.js --dist-dir static/dist --public-url /static/dist/ & parcel watch frontend_src/js/chat-client.js --dist-dir static/dist --public-url /static/dist/",
|
"watch": "npm run prewatch && parcel watch frontend_src/js/main.js --dist-dir static/dist --public-url /static/dist/ & parcel watch frontend_src/js/chat-client.js --dist-dir static/dist --public-url /static/dist/",
|
||||||
"clean": "rm -rf static/dist/* static/assets .parcel-cache"
|
"clean": "rm -rf static/dist/* static/assets .parcel-cache",
|
||||||
|
"postbuild": "node scripts/generate-manifest.mjs"
|
||||||
|
},
|
||||||
|
"targets": {
|
||||||
|
"default": {
|
||||||
|
"context": "browser",
|
||||||
|
"distDir": "static/dist",
|
||||||
|
"publicUrl": "/static/dist/",
|
||||||
|
"outputFormat": "esmodule",
|
||||||
|
"isLibrary": false,
|
||||||
|
"optimize": true,
|
||||||
|
"sourceMap": false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
55
nginx/scripts/generate-manifest.mjs
Normal file
55
nginx/scripts/generate-manifest.mjs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { promises as fs } from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
const DIST_DIR = path.resolve(process.cwd(), 'static', 'dist');
|
||||||
|
const OUT_FILE = path.join(DIST_DIR, 'manifest.json');
|
||||||
|
|
||||||
|
const LOGICAL_ENTRIES = [
|
||||||
|
{ key: 'dist/chat-client.js', base: 'chat-client', exts: ['.js'] },
|
||||||
|
{ key: 'dist/chat-client.css', base: 'chat-client', exts: ['.css'] },
|
||||||
|
// Optionally include main entries if referenced via templates in the future
|
||||||
|
{ key: 'dist/main.js', base: 'main', exts: ['.js'] },
|
||||||
|
{ key: 'dist/main.css', base: 'main', exts: ['.css'] },
|
||||||
|
];
|
||||||
|
|
||||||
|
function isHashedFile(file, base, ext) {
|
||||||
|
return file.startsWith(base + '.') && file.endsWith(ext) && file !== base + ext;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generate() {
|
||||||
|
const files = await fs.readdir(DIST_DIR);
|
||||||
|
const manifest = {};
|
||||||
|
|
||||||
|
for (const entry of LOGICAL_ENTRIES) {
|
||||||
|
const matches = [];
|
||||||
|
for (const ext of entry.exts) {
|
||||||
|
for (const f of files) {
|
||||||
|
if (isHashedFile(f, entry.base, ext)) {
|
||||||
|
matches.push(`dist/${f}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (matches.length === 1) {
|
||||||
|
manifest[entry.key] = matches[0];
|
||||||
|
} else if (matches.length > 1) {
|
||||||
|
const stats = await Promise.all(
|
||||||
|
matches.map(async m => ({
|
||||||
|
file: m,
|
||||||
|
mtime: (await fs.stat(path.join(DIST_DIR, path.basename(m)))).mtimeMs,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
stats.sort((a, b) => b.mtime - a.mtime);
|
||||||
|
manifest[entry.key] = stats[0].file;
|
||||||
|
} else {
|
||||||
|
manifest[entry.key] = entry.key; // fallback zodat pagina niet breekt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.writeFile(OUT_FILE, JSON.stringify(manifest, null, 2), 'utf8');
|
||||||
|
console.log(`[manifest] written: ${OUT_FILE}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
generate().catch(err => {
|
||||||
|
console.error('[manifest] generation failed:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user