Migrate to env-driven templates and slavic-pulse feature set.

Replace hardcoded HTML with .env generation, YouTube thumbnail backgrounds, responsive FHD/vertical intro, hardened nginx, and updated Dokploy deploy flow.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-27 20:36:51 +02:00
parent 808c39d838
commit 22c3df41a8
16 changed files with 498 additions and 113 deletions
+4
View File
@@ -7,3 +7,7 @@ node_modules/
.env .env
.venv/ .venv/
docker-compose-example/ docker-compose-example/
docker-compose*.yml
dev.sh
index.html
site.webmanifest
+35 -3
View File
@@ -1,8 +1,40 @@
# Agencja AI — Environment Configuration (Local Development) # Agencja AI — Environment Configuration
# Copy to .env and fill in your values. # Copy to .env and fill in your values.
# Never commit .env with real credentials to version control. # Never commit .env with real credentials to version control.
#
# Po zmianie wartości uruchom: ./dev.sh config (lub ./dev.sh restart)
# --- Frontend Server --- # --- Frontend Server (TYLKO lokalny dev — nie używaj w Dokploy) ---
# Port na hoście (localhost). W kontenerze nginx zawsze nasłuchuje na 80. # Port na hoście (localhost). W kontenerze nginx zawsze nasłuchuje na 80.
# Mapowanie w docker-compose.yml: ${FRONTEND_PORT}:80
FRONTEND_PORT=8081 FRONTEND_PORT=8081
# --- Strona (meta, SEO, PWA) ---
SITE_TITLE="Agencja AI"
SITE_SHORT_NAME="Agencja AI"
SITE_BASE_URL=https://agencja-ai.com
SITE_DESCRIPTION="Lokalne modele LLM, własny serwer AI (Proxmox, TrueNAS, Ubuntu), RAG, Ollama, Open WebUI, bezpieczny self-hosting i wdrożenia biznesowe lokalnej AI."
# Krótki opis pod og:description (Facebook, LinkedIn) — bez HTML; puste = SITE_DESCRIPTION
FACEBOOK_DESCRIPTION="Lokalne modele LLM (Llama 3, Mistral, Command-R, Phi-3, DeepSeek i inne). Budowa własnego serwera AI na bazie Proxmox, TrueNAS SCALE oraz Ubuntu. Optymalizacja GPU w środowisku Linux (Ubuntu, OpenSUSE, Proxmox). Technologia RAG jak podpiąć Twoją firmową wiedzę do lokalnego AI. Prywatne instancje Ollama, Open WebUI, AnythingLLM, PrivateGPT. Bezpieczny self-hosting od konfiguracji Proxmox VE, przez TrueNAS, po produkcyjne wdrożenia. Porównania sprzętu, zużycie prądu, chłodzenie i realne koszty posiadania własnego AI. Automatyzacje, integracje i zaawansowane zastosowania biznesowe lokalnej AI."
TWITTER_DESCRIPTION="Lokalne modele LLM (Llama 3, Mistral, Command-R, Phi-3, DeepSeek i inne). Budowa własnego serwera AI na bazie Proxmox, TrueNAS SCALE oraz Ubuntu. Optymalizacja GPU w środowisku Linux (Ubuntu, OpenSUSE, Proxmox). Technologia RAG jak podpiąć Twoją firmową wiedzę do lokalnego AI. Prywatne instancje Ollama, Open WebUI, AnythingLLM, PrivateGPT. Bezpieczny self-hosting od konfiguracji Proxmox VE, przez TrueNAS, po produkcyjne wdrożenia. Porównania sprzętu, zużycie prądu, chłodzenie i realne koszty posiadania własnego AI. Automatyzacje, integracje i zaawansowane zastosowania biznesowe lokalnej AI."
SITE_OG_IMAGE_ALT="Agencja AI — lokalna sztuczna inteligencja i self-hosting"
THEME_COLOR="#000000"
# --- Ikony (ścieżki względem roota strony) ---
FAVICON_48=/assets/imgs/favico/48x48.png
FAVICON_180=/assets/imgs/favico/180x180.png
FAVICON_512=/assets/imgs/favico/512x512.png
# --- YouTube ---
# Short FHD 16:9 (landscape / szeroki viewport) — ID z adresu youtube.com/watch?v=...
VIDEO_ID_FHD=kujXXtoHUbc
POST_VIDEO_REDIRECT_URL_FHD=
# Short vertical 9:16 (portrait / wąski viewport) — puste = fallback do FHD
VIDEO_ID_VERTICAL=
POST_VIDEO_REDIRECT_URL_VERTICAL=
# Zachowanie po zakończeniu intro (domyślnie dla obu formatów):
# true = przekieruj na POST_VIDEO_REDIRECT_URL_* (YouTube / inny link)
# false = zostaw miniaturkę YouTube jako tło strony (bez przekierowania)
POST_VIDEO_REDIRECT_ENABLED=false
# Opcjonalnie osobno dla każdego formatu (puste = użyj POST_VIDEO_REDIRECT_ENABLED):
# POST_VIDEO_REDIRECT_ENABLED_FHD=false
# POST_VIDEO_REDIRECT_ENABLED_VERTICAL=false
+4
View File
@@ -2,6 +2,10 @@
.env .env
*.env.local *.env.local
# Generated from .env at dev/build time
index.html
site.webmanifest
# Python # Python
__pycache__/ __pycache__/
.venv/ .venv/
+12 -6
View File
@@ -1,19 +1,25 @@
FROM nginx:1.27-alpine AS production FROM nginx:1.27-alpine AS production
RUN apk add --no-cache gettext
# Remove default nginx config # Remove default nginx config
RUN rm /etc/nginx/conf.d/default.conf RUN rm /etc/nginx/conf.d/default.conf
# Copy nginx config from project root # Copy nginx configs from project root
COPY nginx.conf /etc/nginx/nginx.conf
COPY default.conf /etc/nginx/conf.d/default.conf COPY default.conf /etc/nginx/conf.d/default.conf
# Copy web files (index.html + assets) # Copy static assets
COPY index.html /usr/share/nginx/html/index.html
COPY site.webmanifest /usr/share/nginx/html/site.webmanifest
COPY assets/ /usr/share/nginx/html/assets/ COPY assets/ /usr/share/nginx/html/assets/
# Templates + generator (rendered at container start)
COPY index.html.template site.webmanifest.template generate-from-env.sh /opt/agencja-ai/
RUN chmod +x /opt/agencja-ai/generate-from-env.sh
COPY docker-entrypoint.sh /docker-entrypoint.d/99-generate-from-env.sh
RUN chmod +x /docker-entrypoint.d/99-generate-from-env.sh
EXPOSE 80 EXPOSE 80
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget -qO- http://localhost:80/ || exit 1 CMD wget -qO- http://localhost:80/ || exit 1
CMD ["nginx", "-g", "daemon off;"]
+25 -2
View File
@@ -10,9 +10,24 @@ server {
gzip_types text/plain text/css application/json application/javascript text/xml application/xml image/svg+xml; gzip_types text/plain text/css application/json application/javascript text/xml application/xml image/svg+xml;
gzip_min_length 1024; gzip_min_length 1024;
# Block hidden files and sensitive extensions
location ~ /\. {
deny all;
return 404;
}
location ~* \.(sh|template|env|yml|yaml|md|git|bak|sql|log)$ {
deny all;
return 404;
}
# Main page # Main page
location / { location = / {
try_files $uri $uri/ /index.html; try_files /index.html =404;
}
location = /index.html {
add_header Cache-Control "no-cache";
} }
# Web app manifest # Web app manifest
@@ -29,7 +44,15 @@ server {
try_files $uri =404; try_files $uri =404;
} }
# Everything else → 404 (no fallback to index.html)
location / {
return 404;
}
# Security headers # Security headers
add_header X-Frame-Options SAMEORIGIN always; add_header X-Frame-Options SAMEORIGIN always;
add_header X-Content-Type-Options nosniff always; add_header X-Content-Type-Options nosniff always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://www.youtube.com https://www.gstatic.com; style-src 'self' 'unsafe-inline'; img-src 'self' https://img.youtube.com data:; frame-src https://www.youtube.com; connect-src 'self' https://www.youtube.com; base-uri 'self'; form-action 'none'; object-src 'none'; upgrade-insecure-requests;" always;
} }
+17 -2
View File
@@ -10,12 +10,22 @@ if [[ ! -f .env ]]; then
echo "Utworzono .env z .env.example" echo "Utworzono .env z .env.example"
fi fi
generate_from_env() {
set -a
# shellcheck disable=SC1091
source .env
set +a
chmod +x generate-from-env.sh
./generate-from-env.sh .
}
case "${1:-up}" in case "${1:-up}" in
up) up)
generate_from_env
$COMPOSE up -d $COMPOSE up -d
echo "" echo ""
echo "Dev server: http://localhost:${FRONTEND_PORT:-8081}" echo "Dev server: http://localhost:${FRONTEND_PORT:-8081}"
echo "Edytuj pliki → F5 w przeglądarce (bez rebuildu)." echo "Edytuj .env → ./dev.sh config → F5 w przeglądarce."
;; ;;
down) down)
$COMPOSE down $COMPOSE down
@@ -24,14 +34,19 @@ case "${1:-up}" in
$COMPOSE logs -f $COMPOSE logs -f
;; ;;
restart) restart)
generate_from_env
$COMPOSE restart $COMPOSE restart
;; ;;
config)
generate_from_env
echo "Wygenerowano index.html i site.webmanifest z .env."
;;
reload-nginx) reload-nginx)
docker exec agencja-ai-com-frontend nginx -s reload docker exec agencja-ai-com-frontend nginx -s reload
echo "Nginx przeładowany (po zmianie default.conf)." echo "Nginx przeładowany (po zmianie default.conf)."
;; ;;
*) *)
echo "Użycie: $0 [up|down|logs|restart|reload-nginx]" echo "Użycie: $0 [up|down|logs|restart|reload-nginx|config]"
exit 1 exit 1
;; ;;
esac esac
+23 -5
View File
@@ -1,9 +1,27 @@
# Agencja AI — Environment Configuration (Dokploy) # Agencja AI — Environment Configuration (Dokploy)
# Copy to .env before deploy if needed. # Po zmianie wartości uruchom redeploy w Dokploy.
# Never commit .env with real credentials to version control.
# --- Frontend Server --- # --- Frontend Server ---
# Port publikowany na hoście. W kontenerze nginx zawsze nasłuchuje na 80.
# Mapowanie w docker-compose.yml: ${FRONTEND_PORT}:80
# Dokploy/Traefik dodatkowo routuje po FQDN do wewnętrznego portu 80 kontenera.
FRONTEND_PORT=8081 FRONTEND_PORT=8081
# --- Strona (meta, SEO, PWA) ---
SITE_TITLE="Agencja AI"
SITE_SHORT_NAME="Agencja AI"
SITE_BASE_URL=https://agencja-ai.com
SITE_DESCRIPTION="Lokalne modele LLM, własny serwer AI (Proxmox, TrueNAS, Ubuntu), RAG, Ollama, Open WebUI, bezpieczny self-hosting i wdrożenia biznesowe lokalnej AI."
FACEBOOK_DESCRIPTION="Lokalne modele LLM (Llama 3, Mistral, Command-R, Phi-3, DeepSeek i inne). Budowa własnego serwera AI na bazie Proxmox, TrueNAS SCALE oraz Ubuntu. Optymalizacja GPU w środowisku Linux (Ubuntu, OpenSUSE, Proxmox). Technologia RAG jak podpiąć Twoją firmową wiedzę do lokalnego AI. Prywatne instancje Ollama, Open WebUI, AnythingLLM, PrivateGPT. Bezpieczny self-hosting od konfiguracji Proxmox VE, przez TrueNAS, po produkcyjne wdrożenia. Porównania sprzętu, zużycie prądu, chłodzenie i realne koszty posiadania własnego AI. Automatyzacje, integracje i zaawansowane zastosowania biznesowe lokalnej AI."
TWITTER_DESCRIPTION="Lokalne modele LLM (Llama 3, Mistral, Command-R, Phi-3, DeepSeek i inne). Budowa własnego serwera AI na bazie Proxmox, TrueNAS SCALE oraz Ubuntu. Optymalizacja GPU w środowisku Linux (Ubuntu, OpenSUSE, Proxmox). Technologia RAG jak podpiąć Twoją firmową wiedzę do lokalnego AI. Prywatne instancje Ollama, Open WebUI, AnythingLLM, PrivateGPT. Bezpieczny self-hosting od konfiguracji Proxmox VE, przez TrueNAS, po produkcyjne wdrożenia. Porównania sprzętu, zużycie prądu, chłodzenie i realne koszty posiadania własnego AI. Automatyzacje, integracje i zaawansowane zastosowania biznesowe lokalnej AI."
SITE_OG_IMAGE_ALT="Agencja AI — lokalna sztuczna inteligencja i self-hosting"
THEME_COLOR=#000000
# --- Ikony (ścieżki względem roota strony) ---
FAVICON_48=/assets/imgs/favico/48x48.png
FAVICON_180=/assets/imgs/favico/180x180.png
FAVICON_512=/assets/imgs/favico/512x512.png
# --- YouTube ---
VIDEO_ID_FHD=kujXXtoHUbc
POST_VIDEO_REDIRECT_URL_FHD=
VIDEO_ID_VERTICAL=
POST_VIDEO_REDIRECT_URL_VERTICAL=
POST_VIDEO_REDIRECT_ENABLED=false
+120 -40
View File
@@ -1,59 +1,139 @@
# Agencja AI - Frontend (Dokploy Deployment) # Agencja AI - Frontend (Dokploy Deployment)
Strona internetowa ze statycznym obrazem tła od pierwszej klatki i krótkim intro wideo streamowanym z YouTube (bez widocznych ramek playera) po załadowaniu. Statyczna strona z intro YouTube (short FHD 16:9 lub vertical 9:16 — wybór po formacie viewportu). Tło przed filmem to miniaturka z YouTube. Konfiguracja w `.env`, HTML generowany przy starcie kontenera.
## Struktura projektu ## Struktura projektu
``` ```
site-agencja-ai-com/ site-agencja-ai-com/
├── docker-compose.yml # Lokalny dev (build: context .) ├── docker-compose.yml # Lokalny dev (build z Dockerfile)
├── Dockerfile # Kontener nginx 1.27 Alpine ├── Dockerfile
├── default.conf # Konfiguracja nginx ├── default.conf
├── index.html # Strona ze streamem YouTube ├── nginx.conf
├── assets/imgs/www-bg-agencja-ai-com-001.jpg # Obraz tła (367 KB) ├── index.html.template # Szablon strony
├── assets/imgs/favico/ # Favicony (48, 180, 512 px) ├── generate-from-env.sh # Generator HTML z .env
├── site.webmanifest # Manifest PWA / Android ├── assets/imgs/favico/ # Favicony
├── .dockerignore # Ignorowane pliki przy budowie ├── site.webmanifest.template
├── .gitignore # Git ignore
└── docker-compose-example/ # Pliki deploymentowe Dokploy └── docker-compose-example/ # Pliki deploymentowe Dokploy
├── docker-compose.yml # Dokploy: image + git clone ├── docker-compose.yml
├── .env # Wymagany przez Dokploy (pusty) ├── .env.example
└── README.md # Ten plik └── README.md
``` ```
## Dwa podejścia do deployu ## Dokploy deployment
### 1. Lokalny development (docker-compose.yml w root) Dokploy kopiuje na serwer **tylko** `docker-compose.yml` + `.env` — nie ma tam `Dockerfile` ani pełnego repo. Kontener przy starcie robi `git clone` publicznego repo, generuje HTML z `.env` i uruchamia nginx.
Używa `build:` z Dockerfile — buduje obraz lokalnie. Port hosta ustawiasz w `.env`:
```bash
cp .env.example .env # opcjonalnie zmień FRONTEND_PORT
cd /home/tomasz-syn-grzegorza/Pulpit/cursor/site-agencja-ai-com/
docker compose up -d --build
# Strona na http://localhost:${FRONTEND_PORT} (domyślnie 8081)
```
### 2. Dokploy deployment (docker-compose-example/)
Używa `image:` + runtime git clone — nie wymaga lokalnego build contextu, bo Dokploy kopiuje tylko `docker-compose.yml` i `.env` na serwer.
**Kroki w Dokploy UI:** **Kroki w Dokploy UI:**
1. Nowa aplikacja → wklej zawartość `docker-compose-example/docker-compose.yml` 1. Nowa aplikacja (Compose) → wklej `docker-compose-example/docker-compose.yml`
2. Wklej `.env` z tego samego katalogu (ustaw `FRONTEND_PORT` jeśli potrzebny) 2. Wklej `.env` (na podstawie `.env.example`) — ustaw `FRONTEND_PORT`, `VIDEO_ID_*`, opisy SEO
3. Ustaw FQDN w UI (np. `agencja-ai.com`) 3. **Domains** → dodaj **oba** hosty (Container Port = `80`, HTTPS = Let's Encrypt):
4. Deploy - `agencja-ai.com`
- `www.agencja-ai.com`**wymagane** — bez tego www dostaje domyślny cert Traefika (CN=*, self-signed) i telefony pokazują „serwer nie potrafi udowodnić tożsamości”
4. (Opcjonalnie) **Advanced → Redirects** → preset **www → non-www**
5. Deploy
**Porty:** `FRONTEND_PORT` z `.env` mapuje port hosta na nginx w kontenerze (`${FRONTEND_PORT}:80`). Wewnątrz kontenera nginx zawsze nasłuchuje na porcie 80 (`default.conf`). Dokploy/Traefik routuje po FQDN do tego samego wewnętrznego portu 80. **Porty:** compose mapuje `${FRONTEND_PORT}:80`. Traefik/Dokploy dodatkowo routuje po FQDN do wewnętrznego portu 80 kontenera.
**Repo musi być publiczne** w Gitea — clone bez tokenu. Po `git push` zrób redeploy, żeby kontener pobrał nowy kod.
## Jak to działa ## Jak to działa
1. **Obraz tła od startu** - Lokalny JPG widoczny od pierwszej klatki; loader znika po załadowaniu obrazka 1. **Start kontenera**`git clone` publicznego repo do `/app`
2. **Intro wideo YouTube** - Standardowy embed YouTube (`kujXXtoHUbc`) jako nakładka po załadowaniu playera 2. **`generate-from-env.sh`** — buduje `index.html` i `site.webmanifest` ze zmiennych `.env`
3. **Po zakończeniu filmu** - Warstwa wideo znika, obraz JPG zostaje 3. **Kopiowanie**`assets/`, `default.conf`, `nginx.conf` → nginx
4. **Safety timeout 15s** - Jeśli YouTube nie załaduje się w czasie → strona zostaje na obrazku tła 4. **Cleanup**`rm -rf /app` (brak pełnego repo na dysku kontenera)
5. **Intro wideo** — YouTube IFrame API, autoplay; po filmie redirect lub tło
## Modyfikacja ## Konfiguracja (.env)
- **Inny film:** zmień `VIDEO_ID` w `index.html` | Zmienna | Opis |
- **Inny port hosta:** zmień `FRONTEND_PORT` w `.env` (nginx w kontenerze zostaje na porcie 80) |---------|------|
| `FRONTEND_PORT` | Port na hoście (np. `8081`) |
| `SITE_TITLE`, `SITE_BASE_URL`, `SITE_DESCRIPTION` | Meta i SEO |
| `FACEBOOK_DESCRIPTION`, `TWITTER_DESCRIPTION` | Opisy social media |
| `VIDEO_ID_FHD` / `VIDEO_ID_VERTICAL` | ID shortów YouTube |
| `POST_VIDEO_REDIRECT_URL_*` | URL po zakończeniu filmu |
| `POST_VIDEO_REDIRECT_ENABLED` | `true` = redirect, `false` = zostaje tło |
## Lokalny development
```bash
cp .env.example .env
./dev.sh up
# Strona na http://localhost:${FRONTEND_PORT} (domyślnie 8081)
# Edytuj .env → ./dev.sh config → F5 w przeglądarce
```
Produkcyjny build lokalnie:
```bash
docker compose up -d --build
```
## Hardening bezpieczeństwa (wbudowany)
- Repo publiczne — brak tokena GITEA w kontenerze
- Po starcie `/app` usuwane — brak compose/README/szablonów na dysku
- nginx: whitelist ścieżek, blokada `.env`/`.sh`/`.template`, nagłówki CSP/Referrer-Policy
- CSP z `'unsafe-inline'` dla skryptu YouTube (inline JS w `index.html`)
- `server_tokens off` — ukryta wersja nginx
## Weryfikacja po deployu
```bash
# Strona działa
curl -sI https://agencja-ai.com/ | head -5
# Wrażliwe pliki niedostępne (404)
curl -sI https://agencja-ai.com/.env | grep HTTP
curl -sI https://agencja-ai.com/generate-from-env.sh | grep HTTP
curl -sI https://agencja-ai.com/random-path | grep HTTP
# Nagłówki bezpieczeństwa
curl -sI https://agencja-ai.com/ | grep -iE 'content-security-policy|referrer-policy|permissions-policy'
# W kontenerze (na serwerze)
docker logs agencja-ai-com-frontend --tail 30
docker exec agencja-ai-com-frontend ls /app 2>&1 # powinno: No such file
docker exec agencja-ai-com-frontend head -3 /usr/share/nginx/html/index.html
```
## Rozwiązywanie problemów
### Kontener `Restarting` / unhealthy
```bash
docker logs agencja-ai-com-frontend --tail 50
```
Typowe przyczyny:
- Repo **prywatne** → ustaw jako Public w Gitea
- Brak lub błędny `.env` w Dokploy
- Błąd w `generate-from-env.sh` (sprawdź logi)
### Strona czarna / loader bez końca
- Sprawdź konsolę przeglądarki (CSP / YouTube)
- Upewnij się, że `VIDEO_ID_*` są poprawne w `.env`
- Po zmianie `default.conf` lub szablonu: `git push` + redeploy
### Zmiana konfiguracji po deployu
1. Edytuj `.env` w Dokploy
2. Redeploy / restart kontenera (przy starcie ponownie klonuje repo i generuje HTML)
### Błąd certyfikatu SSL na telefonie („serwer nie potrafi udowodnić…”)
**Diagnoza (stan typowy):**
- `https://agencja-ai.com` — certyfikat Let's Encrypt OK
- `https://www.agencja-ai.com`**self-signed** (Traefik default, CN=`*`) → błąd na Androidzie / iPhone / WebView Facebooka
**Naprawa w Dokploy (zalecane):**
1. **Domains****Add Domain**
2. Host: `www.agencja-ai.com`, Port: `80`, HTTPS: włączone, Certificate: **letsencrypt**
3. Deploy / poczekaj ~30 s na wystawienie certyfikatu
4. Sprawdź: `curl -I https://www.agencja-ai.com` (powinno być HTTP 200 lub 301, bez błędu SSL)
5. (Opcjonalnie) **Advanced → Redirects****www to non-www**
**Facebook / in-app browser:** linki w postach powinny wskazywać `https://agencja-ai.com` (bez www).
+25 -11
View File
@@ -1,12 +1,14 @@
# ============================================================ # ============================================================
# Agencja AI - Docker Compose (Dokploy / manual) # Agencja AI - Docker Compose (Dokploy / manual)
# ============================================================ # ============================================================
# Dokploy-compatible pattern: uses pre-built image + runtime git clone. # Dokploy-compatible pattern: pre-built nginx image + runtime git clone.
# Dokploy only copies docker-compose.yml + .env to the server, so local # Dokploy only copies docker-compose.yml + .env to the server, so local
# build context with a Dockerfile does not work. This approach matches # build context with a Dockerfile does not work.
# other working apps on our servers (e.g. wdj-hermes-frontend-matrix).
# #
# Port hosta: FRONTEND_PORT z .env (domyślnie 8080) → nginx w kontenerze na porcie 80. # Repo publiczne — clone bez tokenu. HTML generowany z .env przy starcie.
# Po setupie /app jest usuwane (brak pełnego repo na dysku kontenera).
#
# Port hosta: FRONTEND_PORT z .env → nginx w kontenerze na porcie 80.
# Dokploy/Traefik routuje po FQDN do wewnętrznego portu 80 kontenera. # Dokploy/Traefik routuje po FQDN do wewnętrznego portu 80 kontenera.
# ============================================================ # ============================================================
@@ -15,19 +17,31 @@ services:
image: nginx:1.27-alpine image: nginx:1.27-alpine
container_name: agencja-ai-com-frontend container_name: agencja-ai-com-frontend
restart: unless-stopped restart: unless-stopped
ports: ports:
- "${FRONTEND_PORT:-8080}:80" - "${FRONTEND_PORT:-8080}:80"
env_file:
- .env
command: > command: >
sh -c " sh -ce "
apk add --no-cache git && echo '[agencja-ai] Installing git + gettext...' &&
apk add --no-cache git gettext &&
echo '[agencja-ai] Cloning repo...' &&
rm -rf /app && rm -rf /app &&
git clone https://gitea.expert-it.agency-ai.dev/tomasz-syn-grzegorza/site-agencja-ai-com.git /app && git clone --depth 1 https://gitea.expert-it.agency-ai.dev/tomasz-syn-grzegorza/site-agencja-ai-com.git /app &&
cp /app/index.html /usr/share/nginx/html/index.html && echo '[agencja-ai] Generating index.html from env...' &&
cp /app/site.webmanifest /usr/share/nginx/html/site.webmanifest && chmod +x /app/generate-from-env.sh &&
TEMPLATE_DIR=/app /app/generate-from-env.sh /usr/share/nginx/html &&
echo '[agencja-ai] Copying assets and nginx config...' &&
cp -r /app/assets /usr/share/nginx/html/assets && cp -r /app/assets /usr/share/nginx/html/assets &&
rm /etc/nginx/conf.d/default.conf && rm -f /etc/nginx/conf.d/default.conf &&
cp /app/default.conf /etc/nginx/conf.d/default.conf && cp /app/default.conf /etc/nginx/conf.d/default.conf &&
cp /app/nginx.conf /etc/nginx/nginx.conf &&
echo '[agencja-ai] Removing cloned repo from container...' &&
rm -rf /app &&
echo '[agencja-ai] Starting nginx...' &&
nginx -g 'daemon off;' nginx -g 'daemon off;'
" "
@@ -36,7 +50,7 @@ services:
interval: 30s interval: 30s
timeout: 10s timeout: 10s
retries: 3 retries: 3
start_period: 45s start_period: 60s
deploy: deploy:
resources: resources:
+2
View File
@@ -5,6 +5,8 @@ services:
frontend: frontend:
image: nginx:1.27-alpine image: nginx:1.27-alpine
build: !reset null build: !reset null
env_file:
- .env
volumes: volumes:
- ./index.html:/usr/share/nginx/html/index.html:ro - ./index.html:/usr/share/nginx/html/index.html:ro
- ./site.webmanifest:/usr/share/nginx/html/site.webmanifest:ro - ./site.webmanifest:/usr/share/nginx/html/site.webmanifest:ro
+2
View File
@@ -8,5 +8,7 @@ services:
dockerfile: Dockerfile dockerfile: Dockerfile
container_name: agencja-ai-com-frontend container_name: agencja-ai-com-frontend
restart: unless-stopped restart: unless-stopped
env_file:
- .env
ports: ports:
- "${FRONTEND_PORT:-8080}:80" - "${FRONTEND_PORT:-8080}:80"
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
TEMPLATE_DIR=/opt/agencja-ai /opt/agencja-ai/generate-from-env.sh /usr/share/nginx/html
rm -rf /opt/agencja-ai
unset GITEA_REPO_URL FRONTEND_PORT 2>/dev/null || true
+68
View File
@@ -0,0 +1,68 @@
#!/bin/sh
# Generuje index.html i site.webmanifest z zmiennych środowiskowych (.env).
set -eu
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
TEMPLATE_DIR="${TEMPLATE_DIR:-$SCRIPT_DIR}"
cd "$SCRIPT_DIR"
if [ -n "${1:-}" ]; then
OUTPUT_DIR="$1"
elif [ -d /usr/share/nginx/html ]; then
OUTPUT_DIR="/usr/share/nginx/html"
else
OUTPUT_DIR="."
fi
# --- Domyślne wartości szablonu ---
SITE_TITLE="${SITE_TITLE:-Agencja AI}"
SITE_SHORT_NAME="${SITE_SHORT_NAME:-Agencja AI}"
SITE_BASE_URL="${SITE_BASE_URL:-https://agencja-ai.com}"
SITE_DESCRIPTION="${SITE_DESCRIPTION:-Lokalne modele LLM, własny serwer AI (Proxmox, TrueNAS, Ubuntu), RAG, Ollama, Open WebUI, bezpieczny self-hosting i wdrożenia biznesowe lokalnej AI.}"
FACEBOOK_DESCRIPTION="${FACEBOOK_DESCRIPTION:-Lokalne modele LLM (Llama 3, Mistral, Command-R, Phi-3, DeepSeek i inne). Budowa własnego serwera AI na bazie Proxmox, TrueNAS SCALE oraz Ubuntu. Optymalizacja GPU w środowisku Linux (Ubuntu, OpenSUSE, Proxmox). Technologia RAG jak podpiąć Twoją firmową wiedzę do lokalnego AI. Prywatne instancje Ollama, Open WebUI, AnythingLLM, PrivateGPT. Bezpieczny self-hosting od konfiguracji Proxmox VE, przez TrueNAS, po produkcyjne wdrożenia. Porównania sprzętu, zużycie prądu, chłodzenie i realne koszty posiadania własnego AI. Automatyzacje, integracje i zaawansowane zastosowania biznesowe lokalnej AI.}"
TWITTER_DESCRIPTION="${TWITTER_DESCRIPTION:-$FACEBOOK_DESCRIPTION}"
SITE_OG_IMAGE_ALT="${SITE_OG_IMAGE_ALT:-Agencja AI — lokalna sztuczna inteligencja i self-hosting}"
THEME_COLOR="${THEME_COLOR:-#000000}"
FAVICON_48="${FAVICON_48:-/assets/imgs/favico/48x48.png}"
FAVICON_180="${FAVICON_180:-/assets/imgs/favico/180x180.png}"
FAVICON_512="${FAVICON_512:-/assets/imgs/favico/512x512.png}"
VIDEO_ID_FHD="${VIDEO_ID_FHD:-${VIDEO_ID:-kujXXtoHUbc}}"
VIDEO_ID_VERTICAL="${VIDEO_ID_VERTICAL:-}"
POST_VIDEO_REDIRECT_URL_FHD="${POST_VIDEO_REDIRECT_URL_FHD:-${POST_VIDEO_REDIRECT_URL:-}}"
POST_VIDEO_REDIRECT_URL_VERTICAL="${POST_VIDEO_REDIRECT_URL_VERTICAL:-}"
POST_VIDEO_REDIRECT_ENABLED="${POST_VIDEO_REDIRECT_ENABLED:-false}"
POST_VIDEO_REDIRECT_ENABLED_FHD="${POST_VIDEO_REDIRECT_ENABLED_FHD:-$POST_VIDEO_REDIRECT_ENABLED}"
POST_VIDEO_REDIRECT_ENABLED_VERTICAL="${POST_VIDEO_REDIRECT_ENABLED_VERTICAL:-$POST_VIDEO_REDIRECT_ENABLED}"
OG_IMAGE_URL="https://img.youtube.com/vi/${VIDEO_ID_FHD}/maxresdefault.jpg"
escape_js() {
printf '%s' "$1" | sed "s/'/\\\\'/g"
}
parse_bool_js() {
case "$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')" in
true|1|yes|on) printf 'true' ;;
*) printf 'false' ;;
esac
}
VIDEO_ID_FHD_JS="$(escape_js "$VIDEO_ID_FHD")"
VIDEO_ID_VERTICAL_JS="$(escape_js "$VIDEO_ID_VERTICAL")"
POST_VIDEO_REDIRECT_URL_FHD_JS="$(escape_js "$POST_VIDEO_REDIRECT_URL_FHD")"
POST_VIDEO_REDIRECT_URL_VERTICAL_JS="$(escape_js "$POST_VIDEO_REDIRECT_URL_VERTICAL")"
POST_VIDEO_REDIRECT_ENABLED_FHD_JS="$(parse_bool_js "$POST_VIDEO_REDIRECT_ENABLED_FHD")"
POST_VIDEO_REDIRECT_ENABLED_VERTICAL_JS="$(parse_bool_js "$POST_VIDEO_REDIRECT_ENABLED_VERTICAL")"
export SITE_TITLE SITE_SHORT_NAME SITE_BASE_URL SITE_DESCRIPTION FACEBOOK_DESCRIPTION TWITTER_DESCRIPTION SITE_OG_IMAGE_ALT
export THEME_COLOR FAVICON_48 FAVICON_180 FAVICON_512 OG_IMAGE_URL
export VIDEO_ID_FHD_JS VIDEO_ID_VERTICAL_JS POST_VIDEO_REDIRECT_URL_FHD_JS POST_VIDEO_REDIRECT_URL_VERTICAL_JS POST_VIDEO_REDIRECT_ENABLED_FHD_JS POST_VIDEO_REDIRECT_ENABLED_VERTICAL_JS
TEMPLATE_VARS='${SITE_TITLE} ${SITE_SHORT_NAME} ${SITE_BASE_URL} ${SITE_DESCRIPTION} ${FACEBOOK_DESCRIPTION} ${TWITTER_DESCRIPTION} ${SITE_OG_IMAGE_ALT} ${THEME_COLOR} ${FAVICON_48} ${FAVICON_180} ${FAVICON_512} ${OG_IMAGE_URL} ${VIDEO_ID_FHD_JS} ${VIDEO_ID_VERTICAL_JS} ${POST_VIDEO_REDIRECT_URL_FHD_JS} ${POST_VIDEO_REDIRECT_URL_VERTICAL_JS} ${POST_VIDEO_REDIRECT_ENABLED_FHD_JS} ${POST_VIDEO_REDIRECT_ENABLED_VERTICAL_JS}'
mkdir -p "$OUTPUT_DIR"
envsubst "$TEMPLATE_VARS" < "$TEMPLATE_DIR/index.html.template" > "$OUTPUT_DIR/index.html"
envsubst "$TEMPLATE_VARS" < "$TEMPLATE_DIR/site.webmanifest.template" > "$OUTPUT_DIR/site.webmanifest"
+119 -36
View File
@@ -3,29 +3,28 @@
<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">
<title>Agencja AI</title> <title>${SITE_TITLE}</title>
<!-- SITE_BASE_URL: https://agencja-ai.com --> <meta name="description" content="${SITE_DESCRIPTION}">
<meta name="description" content="Lokalne modele LLM, własny serwer AI (Proxmox, TrueNAS, Ubuntu), RAG, Ollama, Open WebUI, bezpieczny self-hosting i wdrożenia biznesowe lokalnej AI."> <link rel="canonical" href="${SITE_BASE_URL}/">
<link rel="canonical" href="https://agencja-ai.com/">
<meta property="og:type" content="website"> <meta property="og:type" content="website">
<meta property="og:site_name" content="Agencja AI"> <meta property="og:site_name" content="${SITE_TITLE}">
<meta property="og:locale" content="pl_PL"> <meta property="og:locale" content="pl_PL">
<meta property="og:title" content="Agencja AI"> <meta property="og:title" content="${SITE_TITLE}">
<meta property="og:url" content="https://agencja-ai.com/"> <meta property="og:url" content="${SITE_BASE_URL}/">
<meta property="og:image" content="https://agencja-ai.com/assets/imgs/www-bg-agencja-ai-com-001.jpg"> <meta property="og:image" content="${OG_IMAGE_URL}">
<meta property="og:image:type" content="image/jpeg"> <meta property="og:image:type" content="image/jpeg">
<meta property="og:image:alt" content="Agencja AI — lokalna sztuczna inteligencja i self-hosting"> <meta property="og:image:alt" content="${SITE_OG_IMAGE_ALT}">
<meta property="og:description" content="Lokalne modele LLM (Llama 3, Mistral, Command-R, Phi-3, DeepSeek i inne). Budowa własnego serwera AI na bazie Proxmox, TrueNAS SCALE oraz Ubuntu. Optymalizacja GPU w środowisku Linux (Ubuntu, OpenSUSE, Proxmox). Technologia RAG jak podpiąć Twoją firmową wiedzę do lokalnego AI. Prywatne instancje Ollama, Open WebUI, AnythingLLM, PrivateGPT. Bezpieczny self-hosting od konfiguracji Proxmox VE, przez TrueNAS, po produkcyjne wdrożenia. Porównania sprzętu, zużycie prądu, chłodzenie i realne koszty posiadania własnego AI. Automatyzacje, integracje i zaawansowane zastosowania biznesowe lokalnej AI."> <meta property="og:description" content="${FACEBOOK_DESCRIPTION}">
<meta name="twitter:card" content="summary_large_image"> <meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Agencja AI"> <meta name="twitter:title" content="${SITE_TITLE}">
<meta name="twitter:image" content="https://agencja-ai.com/assets/imgs/www-bg-agencja-ai-com-001.jpg"> <meta name="twitter:image" content="${OG_IMAGE_URL}">
<meta name="twitter:description" content="Lokalne modele LLM (Llama 3, Mistral, Command-R, Phi-3, DeepSeek i inne). Budowa własnego serwera AI na bazie Proxmox, TrueNAS SCALE oraz Ubuntu. Optymalizacja GPU w środowisku Linux (Ubuntu, OpenSUSE, Proxmox). Technologia RAG jak podpiąć Twoją firmową wiedzę do lokalnego AI. Prywatne instancje Ollama, Open WebUI, AnythingLLM, PrivateGPT. Bezpieczny self-hosting od konfiguracji Proxmox VE, przez TrueNAS, po produkcyjne wdrożenia. Porównania sprzętu, zużycie prądu, chłodzenie i realne koszty posiadania własnego AI. Automatyzacje, integracje i zaawansowane zastosowania biznesowe lokalnej AI."> <meta name="twitter:description" content="${TWITTER_DESCRIPTION}">
<link rel="icon" type="image/png" sizes="48x48" href="/assets/imgs/favico/48x48.png"> <link rel="icon" type="image/png" sizes="48x48" href="${FAVICON_48}">
<link rel="icon" type="image/png" sizes="512x512" href="/assets/imgs/favico/512x512.png"> <link rel="icon" type="image/png" sizes="512x512" href="${FAVICON_512}">
<link rel="apple-touch-icon" sizes="180x180" href="/assets/imgs/favico/180x180.png"> <link rel="apple-touch-icon" sizes="180x180" href="${FAVICON_180}">
<link rel="manifest" href="/site.webmanifest"> <link rel="manifest" href="/site.webmanifest">
<meta name="theme-color" content="#000000"> <meta name="theme-color" content="${THEME_COLOR}">
<link rel="preload" as="image" href="/assets/imgs/www-bg-agencja-ai-com-001.jpg"> <link rel="preconnect" href="https://img.youtube.com">
<link rel="preconnect" href="https://www.youtube.com"> <link rel="preconnect" href="https://www.youtube.com">
<link rel="preconnect" href="https://www.google.com"> <link rel="preconnect" href="https://www.google.com">
<style> <style>
@@ -146,7 +145,7 @@
<div id="stage-root"> <div id="stage-root">
<div id="stage"> <div id="stage">
<div id="stage-image" class="stage-layer"> <div id="stage-image" class="stage-layer">
<img src="/assets/imgs/www-bg-agencja-ai-com-001.jpg" alt=""> <img id="stage-bg" src="" alt="${SITE_OG_IMAGE_ALT}">
</div> </div>
<div id="stage-video" class="stage-layer"> <div id="stage-video" class="stage-layer">
<div id="player"></div> <div id="player"></div>
@@ -155,29 +154,84 @@
</div> </div>
<script> <script>
const VIDEO_ID = 'kujXXtoHUbc'; const VIDEO_CONFIGS = {
const BG_IMAGE = '/assets/imgs/www-bg-agencja-ai-com-001.jpg'; fhd: {
id: '${VIDEO_ID_FHD_JS}',
ratio: 16 / 9,
redirectUrl: '${POST_VIDEO_REDIRECT_URL_FHD_JS}',
redirectEnabled: ${POST_VIDEO_REDIRECT_ENABLED_FHD_JS}
},
vertical: {
id: '${VIDEO_ID_VERTICAL_JS}',
ratio: 9 / 16,
redirectUrl: '${POST_VIDEO_REDIRECT_URL_VERTICAL_JS}',
redirectEnabled: ${POST_VIDEO_REDIRECT_ENABLED_VERTICAL_JS}
}
};
let player; let player;
let activeFormat;
let activeConfig;
const loader = document.getElementById('loader'); const loader = document.getElementById('loader');
const stageVideo = document.getElementById('stage-video'); const stageVideo = document.getElementById('stage-video');
const stage = document.getElementById('stage'); const stage = document.getElementById('stage');
const VIDEO_RATIO = 16 / 9;
function isInAppBrowser() {
const ua = navigator.userAgent || '';
return /FBAN|FBAV|Instagram|Line\/|Twitter|LinkedInApp|wv\)/i.test(ua);
}
function hideLoader() {
loader.classList.add('hidden');
}
function youtubeThumbnailUrl(videoId, quality) {
return 'https://img.youtube.com/vi/' + videoId + '/' + (quality || 'maxresdefault') + '.jpg';
}
function updateStageBackground() {
const img = document.getElementById('stage-bg');
if (!img || !activeConfig || !activeConfig.id) return;
delete img.dataset.fallback;
img.onerror = function () {
if (!img.dataset.fallback) {
img.dataset.fallback = '1';
img.src = youtubeThumbnailUrl(activeConfig.id, 'hqdefault');
}
};
img.src = youtubeThumbnailUrl(activeConfig.id);
}
function getVideoFormat() {
const w = stage.clientWidth || window.innerWidth;
const h = stage.clientHeight || window.innerHeight;
return w >= h ? 'fhd' : 'vertical';
}
function resolveConfig(format) {
const config = VIDEO_CONFIGS[format];
if (config.id) return config;
const fallbackKey = format === 'fhd' ? 'vertical' : 'fhd';
const fallback = VIDEO_CONFIGS[fallbackKey];
if (fallback.id) return fallback;
return config;
}
function sizeVideoCover() { function sizeVideoCover() {
const iframe = document.querySelector('#stage-video iframe'); const iframe = document.querySelector('#stage-video iframe');
if (!stage || !iframe) return; if (!stage || !iframe || !activeConfig) return;
const ratio = activeConfig.ratio;
const w = stage.clientWidth; const w = stage.clientWidth;
const h = stage.clientHeight; const h = stage.clientHeight;
let iw, ih; let iw, ih;
if (w / h > VIDEO_RATIO) { if (w / h > ratio) {
iw = w; iw = w;
ih = w / VIDEO_RATIO; ih = w / ratio;
} else { } else {
ih = h; ih = h;
iw = h * VIDEO_RATIO; iw = h * ratio;
} }
iframe.style.width = iw + 'px'; iframe.style.width = iw + 'px';
@@ -189,14 +243,27 @@
iframe.style.margin = '0'; iframe.style.margin = '0';
} }
activeFormat = getVideoFormat();
activeConfig = resolveConfig(activeFormat);
updateStageBackground();
const stageBg = document.getElementById('stage-bg');
const bgPreload = new Image(); const bgPreload = new Image();
bgPreload.onload = () => loader.classList.add('hidden'); bgPreload.onload = hideLoader;
bgPreload.src = BG_IMAGE; bgPreload.onerror = hideLoader;
if (stageBg && stageBg.src) {
bgPreload.src = stageBg.src;
} else {
hideLoader();
}
const tag = document.createElement('script'); const tag = document.createElement('script');
tag.src = 'https://www.youtube.com/iframe_api'; tag.src = 'https://www.youtube.com/iframe_api';
tag.onerror = hideLoader;
document.head.appendChild(tag); document.head.appendChild(tag);
setTimeout(hideLoader, isInAppBrowser() ? 4000 : 15000);
function enableVideoSound() { function enableVideoSound() {
if (!player || typeof player.unMute !== 'function') return; if (!player || typeof player.unMute !== 'function') return;
player.unMute(); player.unMute();
@@ -204,13 +271,15 @@
} }
function onYouTubeIframeAPIReady() { function onYouTubeIframeAPIReady() {
if (!activeConfig || !activeConfig.id) return;
player = new YT.Player('player', { player = new YT.Player('player', {
height: '100%', height: '100%',
width: '100%', width: '100%',
videoId: VIDEO_ID, videoId: activeConfig.id,
playerVars: { playerVars: {
autoplay: 1, autoplay: 1,
mute: 0, mute: 1,
controls: 0, controls: 0,
showinfo: 0, showinfo: 0,
modestbranding: 1, modestbranding: 1,
@@ -231,24 +300,38 @@
function onPlayerReady(event) { function onPlayerReady(event) {
sizeVideoCover(); sizeVideoCover();
enableVideoSound();
stageVideo.classList.add('active'); stageVideo.classList.add('active');
event.target.playVideo(); event.target.playVideo();
if (!isInAppBrowser()) {
enableVideoSound();
}
} }
function onPlayerStateChange(event) { function onPlayerStateChange(event) {
if (event.data === YT.PlayerState.ENDED) { if (event.data === YT.PlayerState.ENDED) {
stageVideo.classList.remove('active'); stageVideo.classList.remove('active');
if (activeConfig.redirectEnabled && activeConfig.redirectUrl) {
window.location.href = activeConfig.redirectUrl;
}
} }
} }
window.addEventListener('resize', sizeVideoCover); function handleResize() {
const newFormat = getVideoFormat();
if (newFormat !== activeFormat) {
activeFormat = newFormat;
activeConfig = resolveConfig(activeFormat);
updateStageBackground();
if (player && activeConfig.id) {
player.loadVideoById(activeConfig.id);
}
}
sizeVideoCover();
}
window.addEventListener('resize', handleResize);
document.getElementById('stage-root').addEventListener('pointerdown', enableVideoSound, { once: true }); document.getElementById('stage-root').addEventListener('pointerdown', enableVideoSound, { once: true });
setTimeout(() => {
loader.classList.add('hidden');
}, 15000);
</script> </script>
</body> </body>
</html> </html>
+30
View File
@@ -0,0 +1,30 @@
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
keepalive_timeout 15;
client_body_timeout 10;
client_header_timeout 10;
send_timeout 10;
server_tokens off;
include /etc/nginx/conf.d/*.conf;
}
@@ -1,24 +1,24 @@
{ {
"name": "Agencja AI", "name": "${SITE_TITLE}",
"short_name": "Agencja AI", "short_name": "${SITE_SHORT_NAME}",
"icons": [ "icons": [
{ {
"src": "/assets/imgs/favico/48x48.png", "src": "${FAVICON_48}",
"sizes": "48x48", "sizes": "48x48",
"type": "image/png" "type": "image/png"
}, },
{ {
"src": "/assets/imgs/favico/180x180.png", "src": "${FAVICON_180}",
"sizes": "180x180", "sizes": "180x180",
"type": "image/png" "type": "image/png"
}, },
{ {
"src": "/assets/imgs/favico/512x512.png", "src": "${FAVICON_512}",
"sizes": "512x512", "sizes": "512x512",
"type": "image/png" "type": "image/png"
} }
], ],
"display": "standalone", "display": "standalone",
"theme_color": "#000000", "theme_color": "${THEME_COLOR}",
"background_color": "#000000" "background_color": "${THEME_COLOR}"
} }