Compare commits

..

60 Commits

Author SHA1 Message Date
valitovgaziz 17b194dd30 fix: nginx healthcheck (wget / instead of curl /health); add backup healthcheck
Deploy / deploy (push) Failing after 2m42s
2026-06-12 12:55:13 +05:00
valitovgaziz e1807167d2 fix(backup): mount rclone.conf to correct path 2026-06-12 12:51:18 +05:00
valitovgaziz 8645342666 fix(backup): use app-network instead of internal for DB access 2026-06-12 12:50:26 +05:00
valitovgaziz 5e4d78b83d add yandex token 2026-06-12 12:49:19 +05:00
Gaziz Valitov ef84eb9a9d fix(certbot): crond -f instead of -b; add rclone.conf 2026-06-12 12:33:02 +05:00
valitovgaziz 3688abb259 fix: nginx entrypoint - use sed instead of grep -oP for Alpine compat 2026-06-12 12:32:06 +05:00
valitovgaziz 8e766b540e feat: CI/CD, per-domain HTTPS, backup, config generator
- sites.yml — единый источник истины для всех сайтов
- generate-configs.sh — генератор nginx конфигов, certbot domains.txt, .env
- nginx: per-domain HTTPS (вместо all-or-nothing switch-config)
- certbot: единый renew-all.sh, динамический init (без 5 дублирующих скриптов)
- backup: контейнер с pg_dump + rclone (Яндекс.Диск), ежедневно в 3AM
- Gitea + Gitea Runner в docker-compose (self-hosted Git + CI/CD)
- .gitea/workflows/deploy.yml — CI/CD pipeline: push → авто-деплой
- Makefile: generate-configs, reconfig, deploy, backup, restore, gitea, help
2026-06-12 12:22:19 +05:00
valitovgaziz abcb327278 docs: add server info and fix easySite paths in AGENTS.md 2026-06-12 11:52:21 +05:00
valitovgaziz 5d22544df1 docs(nginx): remove outdated serv_spa references
Updated the domains table to reflect that yalarba.ru is now
Nuxt 4 SSR (not static Vue SPA), and the old static path
/usr/share/nginx/yalarba/html no longer exists.
2026-06-12 11:27:28 +05:00
valitovgaziz 0898315910 remove legacy serv_spa (yalarba Vue SPA)
- Deleted main_dc/yalarba/serv_spa/ directory
- No docker-compose.yml changes needed (service was already unused)
- Updated docs references to point to yalarba-nuxt
- docker-compose.yml and nginx configs had no references to serv_spa
2026-06-12 11:26:18 +05:00
valitovgaziz eee067f0ca fix: track Dockerfile and .dockerignore in easySite
.gitignore was ignoring these files, causing them to be missing
on fresh checkout (e.g. after git pull on server), which broke
the Docker build. Removed the ignore entries so the files are
tracked by git.
2026-06-12 11:19:33 +05:00
valitovgaziz 2941b14b38 flatten easySite directory: remove extra easySite/easySite nesting
- Moved contents of main_dc/yalarba/easySite/easySite/ up to easySite/
- Updated docker-compose.yml build context path
- Deleted empty nested easySite/ directory
2026-06-12 11:16:15 +05:00
valitovgaziz 888bb2d87b Fix import alias conflict: gormpg + migratepg 2026-06-12 10:59:39 +05:00
valitovgaziz 029812c6a4 Restructure Dockerfiles: copy source before go mod tidy 2026-06-12 10:57:50 +05:00
valitovgaziz 6a60d67b29 Fix Dockerfile: use go mod tidy instead of go mod download 2026-06-12 10:56:45 +05:00
valitovgaziz b0350abfbe DB optimization: pool, golang-migrate, consolidate to single Postgres
- Fix DB_NAME=db_yal -> mydb in api_yal .env
- Add connection pool (MaxOpenConns 25, MaxIdleConns 10, ConnMaxLifetime 30m)
- Replace GORM AutoMigrate with golang-migrate in api_yal and api_bb
- Create embedded SQL migrations for both APIs
- Add DB_SCHEMA support to api_bb config
- Consolidate to single Postgres: db_bb -> schema 'bb' on db container
- Remove db_bb service, bb-network, db_bb volume from compose
- Remove api_tp targets from Makefile
- Clean up old migrate.go
2026-06-12 10:47:41 +05:00
valitovgaziz ec83b97c25 Remove api_tp from docker-compose.yml (service already deleted) 2026-06-12 10:20:13 +05:00
valitovgaziz 86b8968dce add all command for easysite 2026-06-12 10:18:45 +05:00
valitovgaziz 90a96b4125 Migrate easysite from api_es to api_yal
- Remove api_es service, Dockerfile, all Go source files
- Remove api_es from docker-compose.yml, nginx-ssl.conf, .env, Makefile
- Replace nginx /api/ proxy with /api/v1/ → api_yal:8787
- Add amenity/upload domains, AuthResponse, GET /auth/me, GET /objects/my to api_yal
- Rewrite easysite frontend: types, composables, and all 5 pages to use api_yal DTOs
- Wire nuxt.config public.apiBase, add useObjects CRUD composable
- Update docs references from api_es to api_yal
2026-06-12 10:14:38 +05:00
valitovgaziz 64295b689b docs: add Windows test run commands to README.md 2026-06-12 08:51:16 +05:00
valitovgaziz 75198ed00f docs: add integration test run instructions to README.md
- Added section 8 'Тестирование' with run instructions, structure, features, and diagnostics
- Also includes test file route path adjustments and import reordering
2026-06-12 08:46:20 +05:00
valitovgaziz 01e8226c2b Add integration test suite with in-memory SQLite, mock repos, and test server
- Add test_server.go with chi-based router, shared in-memory SQLite DB, mock repositories
- Add mock_object_repository.go and mock_appeal_repository.go for lightweight testing
- Add setup.go with TestConfig/TestUser helpers, HTTP request builder, and fixtures
- Add go-sqlite3 dependency for in-memory test database
- Rewrite all 7 integration test suites (account, appeal, auth, comment, feedback, object, rating)
  using the new test infrastructure
2026-06-12 08:42:04 +05:00
valitovgaziz 4d5090d76c moove font to fonts directory 2026-06-12 02:49:24 +05:00
valitovgaziz 02c6cb680b fix: align API response shapes (items instead of data), add fallbacks to prevent .length crash 2026-06-12 02:31:37 +05:00
valitovgaziz 86f37dde2d add font Bellaboo.woff and change fonts.ccs 2026-06-12 01:57:32 +05:00
valitovgaziz 9c793bad1b fix: align frontend types and forms with api_yal backend (name→first_name+last_name, token→access_token) 2026-06-12 01:41:23 +05:00
valitovgaziz ba7b757541 fix(nginx): preserve /api/v1 prefix when proxying to api_yal, add favicon 2026-06-12 01:23:06 +05:00
valitovgaziz edb7eabd18 On branch main
modified:   main_dc/yalarba/yalarba-nuxt/package-lock.json
	new file:   opencode.json
chagne temperature for opencode into this project
2026-06-12 01:08:14 +05:00
valitovgaziz d8349a0936 feat: add yalarba-nuxt to infra, wire fonts, switch nginx from static SPA to SSR proxy 2026-06-12 00:37:49 +05:00
valitovgaziz 60867af69c feat: create Nuxt 4 SPA for yalarba.ru (yalarba-nuxt) 2026-06-12 00:29:34 +05:00
valitovgaziz 35ba568d97 On branch main
new file:   main_dc/README.md
	modified:   main_dc/valitovgaziz/package-lock.json
	modified:   main_dc/valitovgaziz/src/components/TheFooter.vue
fix 2025 to 2026 into valitovgaziz site
2026-06-10 13:02:00 +05:00
valitovgaziz f06968eb46 fix: add healthcheck to valitovgaziz container 2026-06-10 11:32:22 +05:00
valitovgaziz 075f29cde1 feat: add personal photo to valitovgaziz hero section 2026-06-10 11:24:19 +05:00
valitovgaziz e8a655d54c feat: containerize valitovgaziz site, add Dockerfile, nginx proxy, Makefile targets 2026-06-10 11:03:11 +05:00
valitovgaziz 6ba49127aa feat: add Vue 3 personal site for valitovgaziz in main_dc/my_site 2026-06-10 10:47:59 +05:00
valitovgaziz 2084acb078 redesign valitovgaziz.ru personal site with clean & minimal UI
- Rewrite index.html with modern layout: sticky nav, hero, about, projects, timeline, skills badges, contact
- Consolidate 12 fragmented CSS files into one cohesive style.css with CSS custom properties and dark mode
- Consolidate JS into scripts.js (dark toggle + scroll animations), remove exposed telegram bot token
- Update blog.html to match new design
- Add AGENTS.md
2026-06-10 10:01:55 +05:00
valitovgaziz d1e45c7686 On branch main
modified:   main_dc/yalarba/api_yal/cmd/testrunner/main.go
	modified:   main_dc/yalarba/api_yal/cmd/testrunner/runner.go
	modified:   main_dc/yalarba/api_yal/tests/integration/account_test.go
	modified:   main_dc/yalarba/api_yal/tests/integration/appeal_test.go
	modified:   main_dc/yalarba/api_yal/tests/integration/auth_test.go
	modified:   main_dc/yalarba/api_yal/tests/integration/comment_test.go
	modified:   main_dc/yalarba/api_yal/tests/integration/feedback_test.go
	modified:   main_dc/yalarba/api_yal/tests/integration/object_test.go
	modified:   main_dc/yalarba/api_yal/tests/integration/rating_test.go
	deleted:    main_dc/yalarba/api_yal/tests/testutils/client.go
	modified:   main_dc/yalarba/api_yal/tests/testutils/fixtures.go
	modified:   main_dc/yalarba/api_yal/tests/testutils/setup.go
write comments for and into test's functions
2026-06-08 01:58:04 +05:00
valitovgaziz b4574f9df1 On branch main
modified:   main_dc/yalarba/api_yal/go.mod
	modified:   main_dc/yalarba/api_yal/go.sum
go mod tidy
2026-06-08 01:46:10 +05:00
valitovgaziz 8dfe7e8b4a On branch main
new file:   main_dc/yalarba/api_yal/cmd/testrunner/README.md
	new file:   main_dc/yalarba/api_yal/cmd/testrunner/main.go
	new file:   main_dc/yalarba/api_yal/cmd/testrunner/runner.go
	deleted:    main_dc/yalarba/api_yal/test/intergration/auth_integration_test.go
	deleted:    main_dc/yalarba/api_yal/test/intergration/objects_integration_test.go
	deleted:    main_dc/yalarba/api_yal/test/intergration/setup_test.go
	deleted:    main_dc/yalarba/api_yal/test/setup_test.go
	new file:   main_dc/yalarba/api_yal/tests/integration/account_test.go
	new file:   main_dc/yalarba/api_yal/tests/integration/appeal_test.go
	new file:   main_dc/yalarba/api_yal/tests/integration/auth_test.go
	new file:   main_dc/yalarba/api_yal/tests/integration/comment_test.go
	new file:   main_dc/yalarba/api_yal/tests/integration/feedback_test.go
	new file:   main_dc/yalarba/api_yal/tests/integration/object_test.go
	new file:   main_dc/yalarba/api_yal/tests/integration/rating_test.go
	new file:   main_dc/yalarba/api_yal/tests/testutils/client.go
	new file:   main_dc/yalarba/api_yal/tests/testutils/fixtures.go
	new file:   main_dc/yalarba/api_yal/tests/testutils/setup.go
write tests
2026-06-08 01:44:23 +05:00
valitovgaziz bdf3ba2483 On branch main
modified:   main_dc/yalarba/api_yal/documentation/TEST_REST_API_REQUESTS.md
replace base url
2026-06-07 22:33:35 +05:00
valitovgaziz b98d1f65d3 On branch main
modified:   main_dc/yalarba/api_yal/documentation/README.md
date last commit that set base URL into easysite102.ru doc
2026-06-07 22:15:30 +05:00
valitovgaziz 787f90b5cf On branch main
modified:   documentation/README.md
SET new base URL for api_yal
2026-06-07 21:56:14 +05:00
valitovgaziz d2b77d4553 On branch main
modified:   main_dc/nginx/nginx-ssl.conf
	modified:   main_dc/yalarba/api_es/internal/config/config.go
add config into enginx for api_yal REST_API
2026-06-07 21:42:16 +05:00
valitovgaziz eb5b8fbf26 On branch main
new file:   .gitattributes
	modified:   main_dc/yalarba/api_yal/go.mod
	modified:   main_dc/yalarba/api_yal/go.sum
	deleted:    main_dc/yalarba/api_yal/test/e2e/api_test.go
	deleted:    main_dc/yalarba/api_yal/test/fixtures/test_data.go
	deleted:    main_dc/yalarba/api_yal/test/intergration/account_intergration_test.go
	modified:   main_dc/yalarba/api_yal/test/intergration/setup_test.go
	new file:   main_dc/yalarba/api_yal/test/setup_test.go
create gitattributes text=auto chate LF=CRLF=>auto
create test's file's
2026-06-07 21:10:44 +05:00
valitovgaziz 1bb91820d0 On branch main
new file:   main_dc/yalarba/api_yal/test/e2e/api_test.go
	new file:   main_dc/yalarba/api_yal/test/fixtures/test_data.go
	new file:   main_dc/yalarba/api_yal/test/intergration/account_intergration_test.go
	new file:   main_dc/yalarba/api_yal/test/intergration/auth_integration_test.go
	new file:   main_dc/yalarba/api_yal/test/intergration/objects_integration_test.go
	new file:   main_dc/yalarba/api_yal/test/intergration/setup_test.go
add test files
not implemented
2026-05-31 05:12:49 +05:00
valitovgaziz 9dd4b5f067 On branch main
new file:   main_dc/yalarba/api_yal/documentation/TEST_REST_API_REQUESTS.md
add tests docs
2026-05-27 14:09:18 +05:00
valitovgaziz 5c34816359 On branch main
modified:   main_dc/yalarba/api_yal/documentation/README.md
Add the patch numbers
2026-05-21 10:16:18 +05:00
valitovgaziz 5eb2f5220b On branch main
new file:   main_dc/yalarba/api_yal/documentation/README.md
Add documentation for endpoints for now
`1
2026-05-21 10:15:02 +05:00
valitovgaziz 318075d686 On branch main
modified:   internal/domain/appeal/dto.go
	new file:   internal/domain/appeal/handler.go
	modified:   internal/domain/appeal/router.go
	modified:   internal/domain/appeal/service.go
	modified:   internal/models/appeal.go
	modified:   internal/router/router.go
fix bag with no embeded the Base into appeal
2026-05-21 05:04:34 +05:00
valitovgaziz ba2e3b9545 On branch main
modified:   main_dc/yalarba/api_yal/internal/domain/rating/dto.go
	new file:   main_dc/yalarba/api_yal/internal/domain/rating/handler.go
	new file:   main_dc/yalarba/api_yal/internal/domain/rating/router.go
	new file:   main_dc/yalarba/api_yal/internal/domain/rating/service.go
	modified:   main_dc/yalarba/api_yal/internal/router/router.go
add raing domain without test
2026-05-20 13:23:38 +05:00
valitovgaziz 508eb8b981 On branch main
modified:   main_dc/yalarba/api_yal/internal/domain/object/router.go
	modified:   main_dc/yalarba/api_yal/internal/router/router.go
add register router into main router
2026-05-20 13:17:19 +05:00
valitovgaziz cc3d0a8b07 On branch main
modified:   yalarba/api_yal/internal/domain/account/service.go
	modified:   yalarba/api_yal/internal/domain/comment/dto.go
	new file:   yalarba/api_yal/internal/domain/comment/handler.go
	new file:   yalarba/api_yal/internal/domain/comment/router.go
	new file:   yalarba/api_yal/internal/domain/comment/service.go
	modified:   yalarba/api_yal/internal/repository/feedback_repository.go
	new file:   yalarba/api_yal/internal/util/JSON_resp.go
Realize comment domain hole
2026-05-19 18:11:20 +05:00
valitovgaziz 63d486f48d On branch main
modified:   main_dc/yalarba/api_yal/internal/domain/appeal/router.go
	modified:   main_dc/yalarba/api_yal/internal/domain/feetback/dto.go
	modified:   main_dc/yalarba/api_yal/internal/domain/feetback/handler.go
	modified:   main_dc/yalarba/api_yal/internal/domain/feetback/router.go
	modified:   main_dc/yalarba/api_yal/internal/domain/feetback/service.go
	modified:   main_dc/yalarba/api_yal/internal/models/feedback.go
	modified:   main_dc/yalarba/api_yal/internal/repository/comment_repository.go
	modified:   main_dc/yalarba/api_yal/internal/repository/feedback_repository.go
	modified:   main_dc/yalarba/api_yal/internal/repository/feedback_repository_impl.go
	modified:   main_dc/yalarba/api_yal/internal/router/router.go
craete routerRegister, service, hander, dto for feedback
2026-05-19 15:01:57 +05:00
valitovgaziz 42549eb116 last 2026-05-19 14:16:43 +05:00
valitovgaziz 894415e3ac On branch main
modified:   main_dc/yalarba/api_yal/internal/domain/feetback/dto.go
	modified:   main_dc/yalarba/api_yal/internal/domain/feetback/service.go
last
2026-05-19 14:07:06 +05:00
valitovgaziz e4a1fcfd25 On branch main
modified:   main_dc/yalarba/api_yal/internal/domain/feetback/dto.go
	modified:   main_dc/yalarba/api_yal/internal/domain/feetback/handler.go
	modified:   main_dc/yalarba/api_yal/internal/domain/feetback/router.go
	modified:   main_dc/yalarba/api_yal/internal/domain/feetback/service.go
feedback domain is almost ready
2026-05-19 13:19:47 +05:00
valitovgaziz 4e80d525db Merge branch 'main' of github.com:valitovgaziz/tp 2026-05-18 09:37:32 +05:00
valitovgaziz 8d30480bdc On branch main
new file:   main_dc/yalarba/api_yal/internal/domain/appeal/dto.go
	new file:   main_dc/yalarba/api_yal/internal/domain/appeal/router.go
	new file:   main_dc/yalarba/api_yal/internal/domain/appeal/service.go
	new file:   main_dc/yalarba/api_yal/internal/domain/feetback/handler.go
	new file:   main_dc/yalarba/api_yal/internal/domain/feetback/router.go
	new file:   main_dc/yalarba/api_yal/internal/domain/feetback/service.go
try add domains for appeal
2026-05-18 09:36:53 +05:00
Ildar 4cf8543c82 Fix typo in README documentation location 2026-05-04 15:28:58 +05:00
valitovgaziz bffdf0ec6c On branch main
new file:   documentation/LLM_Information.md
	modified:   main_dc/certbot/scripts/checkRenewCerts.sh
renew checkRenewCerts replace line with expirY_unix parse date formate
2026-05-02 09:15:43 +05:00
592 changed files with 41180 additions and 41057 deletions
+20
View File
@@ -0,0 +1,20 @@
# Нормализовать окончания строк: хранить LF в репозитории
* text=auto
# Явно указать текстовые файлы — Git будет применять конвертацию
*.go text
*.mod text
*.sum text
*.txt text
*.md text
*.json text
*.yml text
*.yaml text
# Бинарные файлы — не трогать окончания строк
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.zip binary
*.exe binary
+52
View File
@@ -0,0 +1,52 @@
name: Deploy
on:
push:
branches: [main]
paths:
- 'main_dc/**'
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy
run: |
cd /home/gaziz/artefacts/tp/main_dc
git pull origin main
# Если изменился sites.yml — генерируем конфиги
if git diff --name-only HEAD~1 HEAD | grep -q 'main_dc/sites.yml'; then
echo "→ sites.yml changed, generating configs..."
bash generate-configs.sh
fi
# Авто-детект и пересборка изменённых сервисов
echo "→ Detecting changed services..."
CHANGED=$(git diff --name-only HEAD~1 HEAD | grep -oP 'main_dc/\K[^/]+' | sort -u)
for svc in $CHANGED; do
svc_name="$svc"
# маппинг директорий на имена compose-сервисов
case "$svc" in
BB) svc_name="api_bb" ;;
valitovgaziz) svc_name="valitovgaziz" ;;
nginx|certbot|backup|gitea) svc_name="$svc" ;;
api_bb|api_yal|analytics|db) svc_name="$svc" ;;
yalarba) svc_name="yalarba" ;;
*) svc_name="" ;;
esac
if [ -n "$svc_name" ] && grep -q "^ $svc_name:" docker-compose.yml; then
echo " → Rebuilding $svc_name..."
make stop_$svc_name build_$svc_name start_$svc_name || \
make stop_$svc build_$svc start_$svc 2>/dev/null || \
true
fi
done
# Nginx всегда перезапускаем если изменились конфиги
if echo "$CHANGED" | grep -q 'nginx\|sites.yml'; then
echo " → Reloading nginx..."
docker compose exec -T nginx nginx -s reload 2>/dev/null || \
docker compose restart nginx
fi
+81
View File
@@ -0,0 +1,81 @@
# AGENTS.md
## Repo overview
Docker Compose hosting for 4 websites (yalarba.ru, begushiybashkir.ru, easysite102.ru, valitovgaziz.ru).
All infrastructure lives under `main_dc/`. Root `package.json` is vestigial — do not use it.
## Directory structure
```
main_dc/
docker-compose.yml -- single compose file orchestrating everything
Makefile -- the primary dev/ops interface; use `make` not raw docker
.env -- shared env: domains, email, api_es port
BB/api_bb/ -- Go REST API (GORM+Chi), port 7777, DB: db_bb (5433)
BB/bbvue/ -- Vue 3 + Vite frontend for begushiybashkir.ru
yalarba/api_tp/ -- Go REST API (GORM+Chi), port 8888, DB: db (5432)
yalarba/api_es/ -- Go REST API (GORM+Chi), port 8088, DB: db (5432)
yalarba/api_yal/ -- Go REST API (GORM+Chi), port 8787, DB: db (5432)
yalarba/easySite/ -- Nuxt 4 SPA for easysite102.ru
yalarba/yalarba-nuxt/ -- Nuxt 4 SPA for yalarba.ru
valitovgaziz/analytics/ -- Node.js (Express) analytics server, port 9999
valitovgaziz/html/ -- static HTML for valitovgaziz.ru
nginx/ -- nginx with automatic HTTP↔HTTPS switching
certbot/ -- Let's Encrypt cert management
stubSite/ -- placeholder site while building
```
## Developer commands (always run from `main_dc/`)
| Command | What it does |
|---|---|
| `make all` | Full cycle: down → git pull → build --no-cache → up -d → watch |
| `make <svc>` | Full cycle for one service, e.g. `make api_bb`, `make nginx`, `make es`, `make analytics` |
| `make bbvue` | Rebuild Vue frontend (calls `npm run build` in `BB/bbvue/`) |
| `make vue_bb` | git pull + npm cache clean + bbvue build + watch |
| `make wn` | `watch -n2 docker ps` — monitor containers |
| `make bb_db` | `psql -U postgres -d bb_db` inside db_bb container |
All `build_*` targets use `--no-cache`.
All full-cycle targets follow: `stop_<svc> → git → build_<svc> → start_<svc> → wn`.
## Frontend dev (outside compose)
```bash
cd main_dc/BB/bbvue && npm run dev # Vite dev server
cd main_dc/BB/bbvue && npm run lint # ESLint --fix
cd main_dc/BB/bbvue && npm run format # Prettier --write src/
# serv_spa удалён — yalarba работает через yalarba-nuxt (Nuxt SSR)
cd main_dc/yalarba/easySite && npm run dev # Nuxt dev
cd main_dc/yalarba/easySite && npm run build # Nuxt build
```
## Service quirks
- **Nginx SSL**: `switch-config.sh` is all-or-nothing — HTTPS only activates when *every* domain has a cert. Until then, SSL port redirects back to HTTP.
- **`yalarba/serv_spa/`**: удалён — был legacy Vue SPA, не использовался.
- **`api_yal`** is the only container that runs as non-root. Runs on port 8787.
- **`api_es`** port is configurable via `API_ES_APP_PORT` in `.env` (default 8088). All other API ports are hardcoded.
- **Databases**: `db` (port 5432) is shared between api_tp, api_es, api_yal. `db_bb` (port 5433) is dedicated to api_bb.
- **GORM auto-migration**: All Go APIs use GORM auto-migrate at startup — no manual migration tooling.
- **Keycloak** referenced in Makefile targets but absent from docker-compose.yml — likely not deployed.
- **`api_yal/testrunner`**: standalone Go test runner binary (not containerized), for running integration test suites.
## Docs convention
READMEs and documentation are primarily in Russian. See `documentation/` for Makefile, Docker, restart, and LLM info docs.
## Server (YalArbaServer)
| Field | Value |
|---|---|
| IP | `94.41.23.97` |
| User | `gaziz` |
| SSH key | `~/.ssh/id_ed25519` (local) |
| SSH | `ssh gaziz@94.41.23.97` |
| Root password | `sudoowneranduser` |
| User `gaziz` password | `sudoowneranduser` |
| Repo path | `/home/gaziz/artefacts/tp/main_dc` |
+1 -1
View File
@@ -38,4 +38,4 @@ yalarba.ru on vue3.js (pinia) need to redevelop on nuxt.js
1. Написать документацию к api всех сайтов
2. Доработать begushiybashkir.ru && easysite102.rr
# документация находиться в директории documentation в корне проекта
# документация находится в директории documentation в корне проекта
+53
View File
@@ -0,0 +1,53 @@
# LLM Information
## Current LLM Configuration
Based on system analysis conducted on 2026-04-16, the following LLM (Large Language Model) is being used:
### Model Details
- **Model Name**: `sourcecraft_model`
- **Current Mode**: Architect (`architect`)
- **Mode Display Name**: 🏗️ Architect
- **System**: SourceCraft Code Assistant Agent
### Environment Context
- **Operating System**: Windows 11
- **Default Shell**: C:\WINDOWS\system32\cmd.exe
- **Workspace Directory**: d:/artifacts/tp
- **User Time Zone**: Asia/Yekaterinburg (UTC+5:00)
### Capabilities
The SourceCraft Code Assistant Agent is an experienced technical leader with capabilities including:
- Information gathering and context analysis
- Detailed planning and task breakdown
- Code writing and modification
- System operations and command execution
- File management and editing
- Web development and debugging
### Modes Available
The system supports multiple specialized modes:
1. **🏗️ Architect** (current) - Planning, design, and strategy
2. **💻 Code** - Code writing, modification, and refactoring
3. **❓ Ask** - Explanations, documentation, and technical questions
4. **🪲 Debug** - Troubleshooting and error diagnosis
5. **🪃 Orchestrator** - Complex multi-step project coordination
### Project Context
The current workspace contains a Docker-based hosting solution for multiple websites:
- yalarba.ru
- begushiybashkir.ru
- easysite102.ru
- valitovgaziz.ru
The project includes backend APIs in Go, frontend applications in Vue.js/Nuxt.js, and various supporting services (nginx, certbot).
### Analysis Method
This information was gathered through:
1. System environment details inspection
2. File structure analysis
3. Configuration file review (package.json, README.md)
4. Current mode and model identification from system metadata
### Last Updated
2026-04-16T15:25:15.218Z
+6 -9
View File
@@ -1,16 +1,13 @@
#CERTBOT NGINX VARIABLES
EMAIL=valitovgaziz@yandex.ru
DOMAINS_yalarba=yalarba.ru,www.yalarba.ru
DOMAINS_valitovgaziz=valitovgaziz.ru,www.valitovgaziz.ru
DOMAINS_easysite102=easysite102.ru,www.easysite102.ru
DOMAINS_begushiybashkir=xn--80abahjtcfl5d0a8di.xn--p1ai,www.xn--80abahjtcfl5d0a8di.xn--p1ai
DOMAINS_begushiybashkir_latin=begushiybashkir.ru,www.begushiybashkir.ru
#CERTBOT NGINX VARIABLES — авто-сгенерировано, не редактировать вручную
ALL_DOMAINS=yalarba.ru,www.yalarba.ru,valitovgaziz.ru,www.valitovgaziz.ru,easysite102.ru,www.easysite102.ru,begushiybashkir.ru,www.begushiybashkir.ru,xn--80abahjtcfl5d0a8di.xn--p1ai,www.xn--80abahjtcfl5d0a8di.xn--p1ai
DOMAINS_begushiybashkir=begushiybashkir.ru,www.begushiybashkir.ru
DOMAINS_begushiybashkir_idn=xn--80abahjtcfl5d0a8di.xn--p1ai,www.xn--80abahjtcfl5d0a8di.xn--p1ai
DOMAINS_easysite102=easysite102.ru,www.easysite102.ru
DOMAINS_valitovgaziz=valitovgaziz.ru,www.valitovgaziz.ru
DOMAINS_yalarba=yalarba.ru,www.yalarba.ru
# keycloak
KEYCLOAK_ADMIN_PASSWORD=your_secure_password
KEYCLOAK_DB_PASSWORD=your_secure_db_password
# API_ES port
API_ES_APP_PORT=8088
+4 -5
View File
@@ -3,13 +3,12 @@ FROM golang:1.26.0-alpine
WORKDIR /app
# Копируем go.mod и go.sum
COPY go.mod go.sum ./
RUN go mod download
# Копируем исходный код
# Копируем весь исходный код
COPY . .
# Скачиваем зависимости
RUN go mod tidy && go mod download
# Компилируем БЕЗ CGO
RUN CGO_ENABLED=0 GOOS=linux go build -o bin/main ./cmd/main.go
+5
View File
@@ -6,6 +6,7 @@ require (
github.com/go-chi/chi/v5 v5.2.3
github.com/go-chi/cors v1.2.2
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/golang-migrate/migrate/v4 v4.18.2
golang.org/x/crypto v0.43.0
gorm.io/driver/postgres v1.6.0
gorm.io/gorm v1.31.0
@@ -15,8 +16,12 @@ require (
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/stretchr/testify v1.11.1 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.10.0 // indirect
golang.org/x/sys v0.37.0 // indirect
)
+2 -6
View File
@@ -32,7 +32,8 @@ func (a *App) Initialize() error {
// Инициализация базы данных
dbConfig := &database.Config{
URL: a.cfg.DatabaseURL,
URL: a.cfg.DatabaseURL,
Schema: a.cfg.DBSchema,
}
a.db = database.NewDatabase(dbConfig)
@@ -46,11 +47,6 @@ func (a *App) Initialize() error {
return err
}
// Выполнение миграций
if err := a.db.Migrate(); err != nil {
return err
}
// Настройка роутера
router := routes.SetupRouter(a.db.DB, a.cfg)
@@ -11,6 +11,7 @@ import (
type Config struct {
Port string
DatabaseURL string
DBSchema string
StaticURL string `env:"STATIC_URL" envDefault:"http://localhost:8080"`
JWTSecret string `env:"JWT_SECRET,required"`
@@ -34,6 +35,7 @@ func Load() *Config {
return &Config{
Port: port,
DatabaseURL: databaseURL,
DBSchema: getEnv("DB_SCHEMA", "public"),
JWTSecret: jwtSecret,
}
}
+61 -17
View File
@@ -1,11 +1,17 @@
package database
import (
"api_bb/migrations"
"database/sql"
"fmt"
"strings"
"time"
"github.com/golang-migrate/migrate/v4"
migratepg "github.com/golang-migrate/migrate/v4/database/postgres"
"github.com/golang-migrate/migrate/v4/source/iofs"
"go.uber.org/zap"
"gorm.io/driver/postgres"
gormpg "gorm.io/driver/postgres"
"gorm.io/gorm"
"api_bb/pkg/logger"
@@ -17,26 +23,34 @@ type Database struct {
}
type Config struct {
URL string
URL string
Schema string
}
func NewDatabase(cfg *Config) *Database {
if cfg.Schema == "" {
cfg.Schema = "public"
}
return &Database{
cfg: cfg,
}
}
// Connect устанавливает соединение с базой данных
func (d *Database) Connect() error {
zapLogger := logger.Get()
// Логирование попытки подключения к БД
zapLogger.Info("attempting to connect to database",
zap.String("host", ExtractHostFromDSN(d.cfg.URL)),
zap.String("database", ExtractDBNameFromDSN(d.cfg.URL)),
zap.String("schema", d.cfg.Schema),
)
db, err := gorm.Open(postgres.Open(d.cfg.URL), &gorm.Config{})
dsn := d.cfg.URL
if d.cfg.Schema != "public" {
dsn = dsn + fmt.Sprintf(" search_path=%s", d.cfg.Schema)
}
db, err := gorm.Open(gormpg.Open(dsn), &gorm.Config{})
if err != nil {
zapLogger.Error("failed to connect to database",
zap.Error(err),
@@ -47,7 +61,21 @@ func (d *Database) Connect() error {
d.DB = db
// Логирование успешного подключения к БД
zapLogger.Info("Configure connection pool")
sqlDB, err := d.DB.DB()
if err != nil {
return fmt.Errorf("failed to get underlying sql.DB: %w", err)
}
sqlDB.SetMaxOpenConns(25)
sqlDB.SetMaxIdleConns(10)
sqlDB.SetConnMaxLifetime(30 * time.Minute)
zapLogger.Info("Run database migrations")
if err := d.runMigrations(sqlDB); err != nil {
return fmt.Errorf("failed to run migrations: %w", err)
}
zapLogger.Info("Migrations completed successfully")
zapLogger.Info("successfully connected to database",
zap.String("host", ExtractHostFromDSN(d.cfg.URL)),
zap.String("database", ExtractDBNameFromDSN(d.cfg.URL)),
@@ -56,7 +84,32 @@ func (d *Database) Connect() error {
return nil
}
// Ping проверяет соединение с базой данных
func (d *Database) runMigrations(sqlDB *sql.DB) error {
zapLogger := logger.Get()
source, err := iofs.New(migrations.FS, ".")
if err != nil {
return fmt.Errorf("failed to create migration source: %w", err)
}
driver, err := migratepg.WithInstance(sqlDB, &migratepg.Config{})
if err != nil {
return fmt.Errorf("failed to create postgres driver: %w", err)
}
m, err := migrate.NewWithInstance("iofs", source, "postgres", driver)
if err != nil {
return fmt.Errorf("failed to create migrate instance: %w", err)
}
if err := m.Up(); err != nil && err != migrate.ErrNoChange {
zapLogger.Error("Migration error", zap.Error(err))
return fmt.Errorf("failed to apply migrations: %w", err)
}
return nil
}
func (d *Database) Ping() error {
zapLogger := logger.Get()
@@ -75,7 +128,6 @@ func (d *Database) Ping() error {
return nil
}
// Close закрывает соединение с базой данных
func (d *Database) Close() error {
zapLogger := logger.Get()
@@ -99,11 +151,7 @@ func (d *Database) Close() error {
return nil
}
// Вспомогательные функции для работы с DSN
// ExtractHostFromDSN извлекает хост из DSN строки
func ExtractHostFromDSN(dsn string) string {
// Простая реализация для PostgreSQL DSN
parts := strings.Split(dsn, " ")
for _, part := range parts {
if strings.HasPrefix(part, "host=") {
@@ -113,9 +161,7 @@ func ExtractHostFromDSN(dsn string) string {
return "unknown"
}
// ExtractDBNameFromDSN извлекает имя базы данных из DSN строки
func ExtractDBNameFromDSN(dsn string) string {
// Простая реализация для PostgreSQL DSN
parts := strings.Split(dsn, " ")
for _, part := range parts {
if strings.HasPrefix(part, "dbname=") {
@@ -125,9 +171,7 @@ func ExtractDBNameFromDSN(dsn string) string {
return "unknown"
}
// MaskPassword маскирует пароль в DSN строке для безопасного логирования
func MaskPassword(dsn string) string {
// Простая реализация - заменяет пароль на ***
parts := strings.Split(dsn, " ")
for i, part := range parts {
if strings.HasPrefix(part, "password=") {
@@ -136,4 +180,4 @@ func MaskPassword(dsn string) string {
}
}
return strings.Join(parts, " ")
}
}
@@ -1,105 +0,0 @@
package database
import (
"go.uber.org/zap"
"api_bb/internal/models"
"api_bb/pkg/logger"
)
// Migrate выполняет автоматические миграции для всех моделей
func (d *Database) Migrate() error {
zapLogger := logger.Get()
zapLogger.Info("starting database migration")
// Список всех моделей для миграции
models := []interface{}{
&models.User{},
&models.News{},
&models.Comment{},
&models.Review{},
&models.UserStats{},
&models.Workout{},
&models.Achievement{},
&models.Event{},
&models.EventRegistration{},
&models.PersonalBest{},
&models.TrainingPlan{},
&models.EmailVerification{},
// Добавьте другие модели здесь
}
for _, model := range models {
modelName := getModelName(model)
zapLogger.Debug("migrating model", zap.String("model", modelName))
if err := d.DB.AutoMigrate(model); err != nil {
zapLogger.Error("failed to migrate model",
zap.String("model", modelName),
zap.Error(err),
)
return err
}
}
zapLogger.Info("database migration completed successfully")
return nil
}
// MigrateModels выполняет миграции для конкретных моделей
func (d *Database) MigrateModels(models ...interface{}) error {
zapLogger := logger.Get()
zapLogger.Info("starting migration for specific models",
zap.Int("model_count", len(models)),
)
for _, model := range models {
modelName := getModelName(model)
zapLogger.Debug("migrating model", zap.String("model", modelName))
if err := d.DB.AutoMigrate(model); err != nil {
zapLogger.Error("failed to migrate model",
zap.String("model", modelName),
zap.Error(err),
)
return err
}
}
zapLogger.Info("models migration completed successfully")
return nil
}
// getModelName возвращает имя модели для логирования
func getModelName(model interface{}) string {
switch model.(type) {
case *models.User:
return "User"
case *models.News:
return "News"
case *models.Comment:
return "Comment"
case *models.Review:
return "Reviews"
case *models.UserStats:
return "Статистика Пользователя"
case *models.Workout:
return "Тренировки пользователя"
case *models.Achievement:
return "Достижения пользователя"
case *models.Event:
return "Событие"
case *models.EventRegistration:
return "Администрирование события"
case *models.PersonalBest:
return "Персональные достижения"
case *models.TrainingPlan:
return "Тренировочный план"
case *models.EmailVerification:
return "Верификация email"
default:
return "Unknown"
}
}
@@ -0,0 +1,14 @@
DROP TABLE IF EXISTS galleries CASCADE;
DROP TABLE IF EXISTS email_verifications CASCADE;
DROP TABLE IF EXISTS training_workouts CASCADE;
DROP TABLE IF EXISTS training_plans CASCADE;
DROP TABLE IF EXISTS personal_bests CASCADE;
DROP TABLE IF EXISTS event_registrations CASCADE;
DROP TABLE IF EXISTS events CASCADE;
DROP TABLE IF EXISTS achievements CASCADE;
DROP TABLE IF EXISTS workouts CASCADE;
DROP TABLE IF EXISTS user_stats CASCADE;
DROP TABLE IF EXISTS reviews CASCADE;
DROP TABLE IF EXISTS comments CASCADE;
DROP TABLE IF EXISTS news CASCADE;
DROP TABLE IF EXISTS users CASCADE;
@@ -0,0 +1,217 @@
CREATE TABLE IF NOT EXISTS users (
id BIGSERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL,
password VARCHAR(255) NOT NULL,
first_name VARCHAR(255) NOT NULL,
last_name VARCHAR(255) NOT NULL,
avatar VARCHAR(255),
phone VARCHAR(255),
experience VARCHAR(255),
goals VARCHAR(255),
newsletter BOOLEAN DEFAULT FALSE,
role VARCHAR(255) DEFAULT 'user',
created_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ,
deleted_at TIMESTAMPTZ,
email_verified BOOLEAN DEFAULT FALSE,
verified_at TIMESTAMPTZ
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_email ON users(email);
CREATE INDEX IF NOT EXISTS idx_users_deleted_at ON users(deleted_at);
CREATE TABLE IF NOT EXISTS news (
id BIGSERIAL PRIMARY KEY,
created_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ,
deleted_at TIMESTAMPTZ,
title VARCHAR(255) NOT NULL,
excerpt VARCHAR(500) NOT NULL,
content TEXT NOT NULL,
image VARCHAR(255),
category VARCHAR(20) NOT NULL,
views BIGINT DEFAULT 0,
author_id BIGINT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_news_deleted_at ON news(deleted_at);
CREATE TABLE IF NOT EXISTS comments (
id BIGSERIAL PRIMARY KEY,
created_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ,
content TEXT NOT NULL,
news_id BIGINT NOT NULL,
author_id BIGINT NOT NULL
);
CREATE TABLE IF NOT EXISTS reviews (
id BIGSERIAL PRIMARY KEY,
created_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ,
deleted_at TIMESTAMPTZ,
rating BIGINT NOT NULL,
text TEXT NOT NULL,
achievement VARCHAR(255),
distance VARCHAR(50),
improvement VARCHAR(100),
trainings BIGINT DEFAULT 0,
verified BOOLEAN DEFAULT FALSE,
author_id BIGINT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_reviews_deleted_at ON reviews(deleted_at);
CREATE TABLE IF NOT EXISTS user_stats (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
total_distance DECIMAL(10,2) DEFAULT 0,
total_time BIGINT DEFAULT 0,
avg_pace VARCHAR(20),
workouts_count BIGINT DEFAULT 0,
current_streak BIGINT DEFAULT 0,
longest_streak BIGINT DEFAULT 0,
weekly_distance DECIMAL(8,2) DEFAULT 0,
monthly_distance DECIMAL(8,2) DEFAULT 0,
best_5k VARCHAR(20),
best_10k VARCHAR(20),
best_half VARCHAR(20),
best_marathon VARCHAR(20),
last_workout TIMESTAMPTZ,
created_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_user_stats_user_id ON user_stats(user_id);
CREATE TABLE IF NOT EXISTS workouts (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
type VARCHAR(20) NOT NULL,
distance_km DECIMAL(5,2) NOT NULL,
duration_min BIGINT NOT NULL,
pace VARCHAR(20),
calories BIGINT DEFAULT 0,
notes TEXT,
date TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ
);
CREATE INDEX IF NOT EXISTS idx_workouts_user_id ON workouts(user_id);
CREATE INDEX IF NOT EXISTS idx_workouts_date ON workouts(date);
CREATE TABLE IF NOT EXISTS achievements (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
type VARCHAR(20) NOT NULL,
title VARCHAR(255) NOT NULL,
description TEXT,
result VARCHAR(100),
distance VARCHAR(50),
date TIMESTAMPTZ NOT NULL,
verified BOOLEAN DEFAULT FALSE,
badge_image VARCHAR(500),
created_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ
);
CREATE INDEX IF NOT EXISTS idx_achievements_user_id ON achievements(user_id);
CREATE TABLE IF NOT EXISTS events (
id BIGSERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
description TEXT NOT NULL,
date TIMESTAMPTZ NOT NULL,
location VARCHAR(255) NOT NULL,
type VARCHAR(50) NOT NULL,
distance VARCHAR(50),
participants_count BIGINT DEFAULT 0,
max_participants BIGINT DEFAULT 0,
registration_open BOOLEAN DEFAULT TRUE,
image VARCHAR(500),
created_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ
);
CREATE TABLE IF NOT EXISTS event_registrations (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
event_id BIGINT NOT NULL,
status VARCHAR(50) DEFAULT 'pending',
notes TEXT,
result_time VARCHAR(20),
created_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ
);
CREATE TABLE IF NOT EXISTS personal_bests (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
distance_type VARCHAR(20) NOT NULL,
time VARCHAR(20) NOT NULL,
pace VARCHAR(20),
date TIMESTAMPTZ NOT NULL,
verified BOOLEAN DEFAULT FALSE,
event_name VARCHAR(255),
location VARCHAR(255),
created_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ
);
CREATE INDEX IF NOT EXISTS idx_personal_bests_user_id ON personal_bests(user_id);
CREATE TABLE IF NOT EXISTS training_plans (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
title VARCHAR(255) NOT NULL,
description TEXT,
weeks BIGINT NOT NULL DEFAULT 12,
workouts_per_week BIGINT NOT NULL DEFAULT 3,
target_distance VARCHAR(50),
target_date TIMESTAMPTZ,
current_week BIGINT DEFAULT 1,
completed BOOLEAN DEFAULT FALSE,
created_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ
);
CREATE INDEX IF NOT EXISTS idx_training_plans_user_id ON training_plans(user_id);
CREATE TABLE IF NOT EXISTS training_workouts (
id BIGSERIAL PRIMARY KEY,
plan_id BIGINT NOT NULL,
week BIGINT NOT NULL,
day BIGINT NOT NULL,
type VARCHAR(20) NOT NULL,
description TEXT,
distance_km DECIMAL(5,2),
duration_min BIGINT,
completed BOOLEAN DEFAULT FALSE,
completed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ
);
CREATE INDEX IF NOT EXISTS idx_training_workouts_plan_id ON training_workouts(plan_id);
CREATE TABLE IF NOT EXISTS email_verifications (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
token VARCHAR(100) NOT NULL,
email VARCHAR(255) NOT NULL,
type VARCHAR(20) NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
used BOOLEAN DEFAULT FALSE,
created_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ,
deleted_at TIMESTAMPTZ
);
CREATE INDEX IF NOT EXISTS idx_email_verifications_user_id ON email_verifications(user_id);
CREATE UNIQUE INDEX IF NOT EXISTS idx_email_verifications_token ON email_verifications(token);
CREATE INDEX IF NOT EXISTS idx_email_verifications_deleted_at ON email_verifications(deleted_at);
CREATE TABLE IF NOT EXISTS galleries (
id BIGSERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
description TEXT,
image_path VARCHAR(500) NOT NULL,
category VARCHAR(20) NOT NULL,
author_id BIGINT NOT NULL,
event_date TIMESTAMPTZ,
views BIGINT DEFAULT 0,
likes BIGINT DEFAULT 0,
created_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ
);
CREATE INDEX IF NOT EXISTS idx_galleries_author_id ON galleries(author_id);
+6
View File
@@ -0,0 +1,6 @@
package migrations
import "embed"
//go:embed *.sql
var FS embed.FS
+1 -1
View File
@@ -30,7 +30,7 @@
│ Docker Compose Cluster │
├─────────────────────────────────────────────────────────────┤
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Nginx │ │ API_TP │ │ API_BB │ │ API_ES │ │
│ │ Nginx │ │ API_TP │ │ API_BB │ │ API_YAL │ │
│ │ (Proxy) │◄─┤(Yalarba) │ │(Бег.Баш)│ │(Easysite)│ │
│ └────┬─────┘ └──────────┘ └──────────┘ └──────────┘ │
│ │ │ │ │ │
+158 -32
View File
@@ -139,6 +139,9 @@ easysite_build:
easysite_start:
docker compose up easysite -d && docker ps
# all
easysite: easysite_stop git easysite_build easysite_start easysite_logs
# Мониторинг системных ресурсов
top:
htop
@@ -165,21 +168,6 @@ restart_analytics:
# Полный цикл обновления analytics
analytics: stop_analitics git build_analititcs start_analytics wn
# Остановка api_es
stop_api_es:
docker compose down api_es
# Пересборка api_es
build_api_es:
docker compose build api_es --no-cache
# Запуск api_es
start_api_es:
docker compose up api_es -d
# Полный цикл обновления api_es
api_es: stop_api_es git build_api_es start_api_es wn
# Остановка certbot
stop_cerbot:
docker compose down certbot
@@ -195,26 +183,32 @@ start_certbot:
# Полный цикл обновления certbot
certbot: stop_cerbot git build_certbot start_certbot wat
# Сборка фронтенда valitovgaziz
valitovgaziz_build_spa: git
cd valitovgaziz && npm run build
# Остановка valitovgaziz
stop_valitovgaziz:
docker compose down valitovgaziz
# Пересборка valitovgaziz
build_valitovgaziz:
docker compose build valitovgaziz --no-cache
# Запуск valitovgaziz
start_valitovgaziz:
docker compose up valitovgaziz -d
# Полный цикл обновления valitovgaziz
valitovgaziz: stop_valitovgaziz git build_valitovgaziz start_valitovgaziz wn
# Сборка SPA + полный цикл обновления valitovgaziz
vue_site: valitovgaziz_build_spa stop_valitovgaziz build_valitovgaziz start_valitovgaziz wn
# Мониторинг состояния контейнеров каждые 2 секунды
wn:
watch -n 2 'docker ps'
# Остановка api_tp
stop_api_tp:
docker compose down api_tp
# Пересборка api_tp
build_api_tp:
docker compose build api_tp --no-cache
# Запуск api_tp
start_api_tp:
docker compose up api_tp -d
# Полный цикл обновления api_tp
api_tp: stop_api_tp git build_api_tp start_api_tp wn
# Остановка api_yal
stop_api_yal:
docker compose down api_yal
@@ -228,4 +222,136 @@ start_api_yal:
docker compose up api_yal -d
# Полный цикл обновления api_yal
api_yal: stop_api_yal git build_api_yal start_api_yal wn
api_yal: stop_api_yal git build_api_yal start_api_yal wn
# Остановка yalarba-nuxt
stop_yalarba:
docker compose down yalarba
# Пересборка yalarba-nuxt
build_yalarba:
docker compose build yalarba --no-cache
# Запуск yalarba-nuxt
start_yalarba:
docker compose up yalarba -d
# Полный цикл обновления yalarba-nuxt
yalarba: stop_yalarba git build_yalarba start_yalarba wn
# ═══════════════════════════════════════════════
# НОВЫЕ ЦЕЛИ: generate-configs, deploy, backup
# ═══════════════════════════════════════════════
# Генерация конфигов из sites.yml
generate-configs:
bash generate-configs.sh
# Генерация + рестарт nginx
reconfig: generate-configs
docker compose restart nginx
$(MAKE) wn
# Авто-детект изменённых сервисов и деплой только их
deploy: git
@echo "=== Detecting changes ==="
@CHANGED=$$(git diff --name-only HEAD~1 HEAD | grep -oP 'main_dc/\K[^/]+' | sort -u); \
for svc in $$CHANGED; do \
case "$$svc" in \
BB) name="api_bb" ;; \
certbot) name="certbot" ;; \
backup) name="backup" ;; \
gitea) name="gitea" ;; \
*) name="$$svc" ;; \
esac; \
if grep -q "^ $$name:" docker-compose.yml 2>/dev/null; then \
echo " → Rebuilding $$name..."; \
$(MAKE) stop_$$name build_$$name start_$$name 2>/dev/null || \
$(MAKE) stop_$$svc build_$$svc start_$$svc 2>/dev/null || true; \
fi; \
done; \
if echo "$$CHANGED" | grep -q 'sites.yml\|nginx'; then \
echo " → Regenerating configs..."; \
bash generate-configs.sh; \
docker compose restart nginx; \
fi
# Ручной запуск бэкапа
backup:
docker compose exec backup /opt/backup.sh
# Ручной запуск бэкапа (разовый контейнер)
backup-run:
docker compose run --rm backup /opt/backup.sh
# Восстановление из бэкапа: make restore [DATE=2026-06-11]
restore:
docker compose run --rm backup /opt/restore.sh $(DATE)
# Gitea — полный цикл обновления
gitea: stop_gitea git build_gitea start_gitea wn
stop_gitea:
docker compose down gitea
build_gitea:
docker compose build gitea --no-cache
start_gitea:
docker compose up gitea -d
# Gitea Runner — полный цикл
gitea-runner: stop_gitea-runner git build_gitea-runner start_gitea-runner wn
stop_gitea-runner:
docker compose down gitea-runner
build_gitea-runner:
docker compose build gitea-runner --no-cache
start_gitea-runner:
docker compose up gitea-runner -d
# Gitea first-time setup helper
gitea-setup:
@echo "=== Gitea Setup ==="
@echo "1. Open http://94.41.23.97:3001 in browser"
@echo "2. Complete initial setup (DB: SQLite3 is fine)"
@echo "3. Create admin user"
@echo "4. Create new repository 'tp' and push:"
@echo " git remote add gitea http://94.41.23.97:3001/USER/tp.git"
@echo " git push -u gitea main"
@echo "5. Register runner:"
@echo " Settings → Actions → Runners → Create Token"
@echo " Update GITEA_RUNNER_REGISTRATION_TOKEN in docker-compose.yml"
@echo " Then: docker compose up -d gitea-runner"
@echo "6. Add secrets in repo Settings → Actions → Secrets:"
@echo " (none needed — runner runs locally)"
# Показать все доступные цели
help:
@echo "=== Make targets ==="
@echo ""
@echo "Site management:"
@echo " generate-configs — generate nginx configs from sites.yml"
@echo " reconfig — generate configs + restart nginx"
@echo ""
@echo "Deploy:"
@echo " all — full cycle all services"
@echo " deploy — auto-detect changes, rebuild only changed"
@echo " <service> — full cycle for one service"
@echo ""
@echo "Backup:"
@echo " backup — run backup via running container"
@echo " backup-run — run backup in one-shot container"
@echo " restore DATE=... — restore from backup"
@echo ""
@echo "Gitea:"
@echo " gitea — full cycle Gitea"
@echo " gitea-runner — full cycle Runner"
@echo " gitea-setup — first-time setup instructions"
@echo ""
@echo "Monitoring:"
@echo " wn — watch docker ps"
@echo " logs_<service> — logs for a service"
@echo " bb_db — psql into bb_db"
+223
View File
@@ -0,0 +1,223 @@
Текущие проблемы
1. Деплой в 5 шагов: локальный код → push → SSH → pull → make (ручной, медленный)
2. Добавление нового сайта требует правки 7+ файлов: .env, docker-compose.yml, nginx-http.conf, nginx-ssl.conf, switch-config.sh, init-certbot.sh, checkRenewCerts.sh, Makefile — легко забыть что-то
3. HTTPS all-or-nothing: если у одного домена нет сертификата — все сайты падают на HTTP
4. Certbot: дублирование кода на каждый домен в init-certbot.sh, checkRenewCerts.sh и 5 отдельных renew-скриптов
5. Makefile растёт — на каждый сервис 4-5 целей
Предложение
Фаза 1: CI/CD через GitHub Actions
Создать .github/workflows/deploy.yml:
name: Deploy
on:
push:
branches: [main]
paths:
- 'main_dc/**'Текущие проблемы
1. Деплой в 5 шагов: локальный код → push → SSH → pull → make (ручной, медленный)
2. Добавление нового сайта требует правки 7+ файлов: .env, docker-compose.yml, nginx-http.conf, nginx-ssl.conf, switch-config.sh, init-certbot.sh, checkRenewCerts.sh, Makefile — легко забыть что-то
3. HTTPS all-or-nothing: если у одного домена нет сертификата — все сайты падают на HTTP
4. Certbot: дублирование кода на каждый домен в init-certbot.sh, checkRenewCerts.sh и 5 отдельных renew-скриптов
5. Makefile растёт — на каждый сервис 4-5 целей
Предложение
Фаза 1: CI/CD через GitHub Actions
Создать .github/workflows/deploy.yml:
name: Deploy
on:
push:
branches: [main]
paths:
- 'main_dc/**'
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy via SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USER }}
key: ${{ secrets.SSH_KEY }}
script: |
cd /home/valitovgaziz/tp
git pull origin main
# Определить какие сервисы изменились и пересобрать только их
# Или просто: make all
Преимущества: push → авто-деплой за ~1 мин, без SSH вручную.
Фаза 2: Единый источник истины — sites.yml
Ввести файл main_dc/sites.yml со списком всех сайтов:
sites:
yalarba:
domain: yalarba.ru
aliases: [www.yalarba.ru]
type: spa
root: /usr/share/nginx/yalarba/html
api: http://api_tp/
api_yal: http://api_yal/
valitovgaziz:
domain: valitovgaziz.ru
aliases: [www.valitovgaziz.ru]
type: container
upstream: http://valitovgaziz/
api: http://analytics:3000/
easysite102:
domain: easysite102.ru
aliases: [www.easysite102.ru]
type: container
upstream: http://easysite:3000
api: http://api_yal:8787/
begushiybashkir:
domain: begushiybashkir.ru
aliases: [www.begushiybashkir.ru]
type: spa
root: /usr/share/nginx/begushiybashkir/html
api: http://api_bb:8080/
Скрипт-генератор (generate-configs.sh) на основе sites.yml создаёт:
- nginx-http.conf — HTTP-блоки
- nginx-ssl.conf — HTTPS-блоки для каждого домена, каждый независимо проверяет свой сертификат
- nginx-partial-ssl.conf — комбинированный: HTTPS где есть серт, HTTP где нет
- certbot-domains.txt — список доменов для certbot
- .env — переменные окружения
Добавление нового сайта: 1 правка в sites.yml → ./generate-configs.sh → git commit.
Фаза 3: Умный nginx — per-domain HTTPS
Убрать all-or-nothing switch-config.sh. Вместо этого nginx грузит все конфиги через include:
/etc/nginx/conf.d/
├── 00-http-default.conf # HTTP на 80
├── 10-yalarba-ssl.conf # HTTPS, если есть серт
├── 20-valitovgaziz-ssl.conf # HTTPS, если есть серт
├── 30-easysite102-ssl.conf # HTTPS, если есть серт
└── common/ # Общие настройки
Каждый файл SSL генерируется с if-проверкой наличия сертификата:
server {
listen 443 ssl;
server_name valitovgaziz.ru www.valitovgaziz.ru;
ssl_certificate /etc/letsencrypt/live/valitovgaziz.ru/fullchain.pem;
...
}
А HTTP-сервер делает return 301 https://$host$request_uri только для тех доменов, у кого есть сертификат. Остальные работают по HTTP.
Фаза 4: Certbot — один скрипт для всех
Вместо 5 renew-скриптов:
# /opt/renew-all.sh — единый скрипт
certbot renew --webroot -w /var/www/certbot
Certbot сам знает все домены, для которых получал сертификаты. --webroot с одним -w работает для всех. Убрать:
- renewBegushiyBAshkirLatin.sh, renewBegushiyBashkir.sh, renewEasysite102.sh, renewValitovGazizCert.sh, renewYalarbaCert.sh
- Заменить checkRenewCerts.sh на вызов certbot renew
Фаза 5: Makefile — авто-детект изменений
Оставить базовые цели, но добавить:
deploy: git
@echo "Detecting changes..."
@git diff --name-only HEAD~1 HEAD | grep -o 'main_dc/[^/]*/' | sort -u | while read dir; do \
service=$$(basename $$dir); \
if grep -q "^ $$service:" docker-compose.yml; then \
make stop_$$service build_$$service start_$$service; \
fi \
done
Или ещё проще: GitHub Actions сам определяет какие сервисы изменились и запускает только их make цели.
Дорожная карта
Шаг Что делаем Эффект
1 sites.yml + generate-configs.sh Добавление сайта = 1 файл
2 Переход на per-domain HTTPS в nginx Один сайт без серта не ломает другие
3 Упрощение certbot: единый certbot renew Удалить 5 скриптов
4 GitHub Actions deploy workflow push → авто-деплой
5 Makefile: deploy с авто-детектом Быстрый частичный деплой
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy via SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USER }}
key: ${{ secrets.SSH_KEY }}
script: |
cd /home/valitovgaziz/tp
git pull origin main
# Определить какие сервисы изменились и пересобрать только их
# Или просто: make all
Преимущества: push → авто-деплой за ~1 мин, без SSH вручную.
Фаза 2: Единый источник истины — sites.yml
Ввести файл main_dc/sites.yml со списком всех сайтов:
sites:
yalarba:
domain: yalarba.ru
aliases: [www.yalarba.ru]
type: spa
root: /usr/share/nginx/yalarba/html
api: http://api_tp/
api_yal: http://api_yal/
valitovgaziz:
domain: valitovgaziz.ru
aliases: [www.valitovgaziz.ru]
type: container
upstream: http://valitovgaziz/
api: http://analytics:3000/
easysite102:
domain: easysite102.ru
aliases: [www.easysite102.ru]
type: container
upstream: http://easysite:3000
api: http://api_yal:8787/
begushiybashkir:
domain: begushiybashkir.ru
aliases: [www.begushiybashkir.ru]
type: spa
root: /usr/share/nginx/begushiybashkir/html
api: http://api_bb:8080/
Скрипт-генератор (generate-configs.sh) на основе sites.yml создаёт:
- nginx-http.conf — HTTP-блоки
- nginx-ssl.conf — HTTPS-блоки для каждого домена, каждый независимо проверяет свой сертификат
- nginx-partial-ssl.conf — комбинированный: HTTPS где есть серт, HTTP где нет
- certbot-domains.txt — список доменов для certbot
- .env — переменные окружения
Добавление нового сайта: 1 правка в sites.yml → ./generate-configs.sh → git commit.
Фаза 3: Умный nginx — per-domain HTTPS
Убрать all-or-nothing switch-config.sh. Вместо этого nginx грузит все конфиги через include:
/etc/nginx/conf.d/
├── 00-http-default.conf # HTTP на 80
├── 10-yalarba-ssl.conf # HTTPS, если есть серт
├── 20-valitovgaziz-ssl.conf # HTTPS, если есть серт
├── 30-easysite102-ssl.conf # HTTPS, если есть серт
└── common/ # Общие настройки
Каждый файл SSL генерируется с if-проверкой наличия сертификата:
server {
listen 443 ssl;
server_name valitovgaziz.ru www.valitovgaziz.ru;
ssl_certificate /etc/letsencrypt/live/valitovgaziz.ru/fullchain.pem;
...
}
А HTTP-сервер делает return 301 https://$host$request_uri только для тех доменов, у кого есть сертификат. Остальные работают по HTTP.
Фаза 4: Certbot — один скрипт для всех
Вместо 5 renew-скриптов:
# /opt/renew-all.sh — единый скрипт
certbot renew --webroot -w /var/www/certbot
Certbot сам знает все домены, для которых получал сертификаты. --webroot с одним -w работает для всех. Убрать:
- renewBegushiyBAshkirLatin.sh, renewBegushiyBashkir.sh, renewEasysite102.sh, renewValitovGazizCert.sh, renewYalarbaCert.sh
- Заменить checkRenewCerts.sh на вызов certbot renew
Фаза 5: Makefile — авто-детект изменений
Оставить базовые цели, но добавить:
deploy: git
@echo "Detecting changes..."
@git diff --name-only HEAD~1 HEAD | grep -o 'main_dc/[^/]*/' | sort -u | while read dir; do \
service=$$(basename $$dir); \
if grep -q "^ $$service:" docker-compose.yml; then \
make stop_$$service build_$$service start_$$service; \
fi \
done
Или ещё проще: GitHub Actions сам определяет какие сервисы изменились и запускает только их make цели.
Дорожная карта
Шаг Что делаем Эффект
1 sites.yml + generate-configs.sh Добавление сайта = 1 файл
2 Переход на per-domain HTTPS в nginx Один сайт без серта не ломает другие
3 Упрощение certbot: единый certbot renew Удалить 5 скриптов
4 GitHub Actions deploy workflow push → авто-деплой
5 Makefile: deploy с авто-детектом Быстрый частичный деплой
+11
View File
@@ -0,0 +1,11 @@
FROM alpine:3.19
RUN apk add --no-cache postgresql-client rclone bash curl
COPY scripts/ /opt/
RUN chmod +x /opt/*.sh
# crontab для расписания бэкапов
RUN echo "$BACKUP_TIME /opt/backup.sh > /proc/1/fd/1 2>&1" > /etc/crontabs/root
CMD ["crond", "-f", "-l", "2"]
+3
View File
@@ -0,0 +1,3 @@
[yadisk]
type = yandex
token = {"access_token":"y0__wgBEI6uquABGMKlCyC2ru7zFztUXB9VV10fCqLpn1iMh9-P7HDo","token_type":"bearer","refresh_token":"2:AAA:AAAAABwKlw4:1:XOD3WRFNbRzP_QWC:hVdNSjVSdfjzZNOXQy6eH7El9bRfWPxzGXvI99qACcdHl7qJrDbAug38IdTRnqglIcni00y1TA:Zbl9G33wrF55KgVeFtfgDQ","expiry":"2027-06-12T12:38:51.056926299+05:00"}
+8
View File
@@ -0,0 +1,8 @@
# Пример конфига rclone для Яндекс.Диска
# Скопируй в backup/rclone.conf и заполни токен
# Инструкция: https://rclone.org/yandex/
[yadisk]
type = yandex
client_id =
client_secret =
token = {"access_token":"...","token_type":"...","expiry":"..."}
+47
View File
@@ -0,0 +1,47 @@
#!/bin/bash
# backup.sh — ежедневный бэкап: pg_dump + файлы → локально + Яндекс.Диск
set -euo pipefail
BACKUP_DIR="/backups/$(date +%Y-%m-%d)"
RETENTION_DAYS="${BACKUP_RETENTION_DAYS:-7}"
DB_NAMES="${DB_NAMES:-mydb}"
TIMESTAMP=$(date +%H%M%S)
mkdir -p "$BACKUP_DIR/db" "$BACKUP_DIR/files"
echo "=== Backup $TIMESTAMP ==="
# 1. Дампы всех БД
IFS=',' read -ra databases <<< "$DB_NAMES"
for db in "${databases[@]}"; do
db=$(echo "$db" | xargs) # trim
echo "→ Dumping database: $db"
PGPASSWORD="$DB_PASSWORD" pg_dump -h "$DB_HOST" -p "${DB_PORT:-5432}" \
-U "$DB_USER" -d "$db" --format=custom \
-f "$BACKUP_DIR/db/${db}-${TIMESTAMP}.dump"
done
# 2. Архив файлов
echo "→ Archiving files..."
tar -czf "$BACKUP_DIR/files/certbot-${TIMESTAMP}.tar.gz" -C /data/certbot . 2>/dev/null || true
tar -czf "$BACKUP_DIR/files/uploads-${TIMESTAMP}.tar.gz" -C /data/uploads . 2>/dev/null || true
tar -czf "$BACKUP_DIR/files/analytics-${TIMESTAMP}.tar.gz" -C /data/analytics . 2>/dev/null || true
# 3. Создаём symlink latest
rm -f /backups/latest
ln -sf "$BACKUP_DIR" /backups/latest
# 4. Ротация — удаляем старше RETENTION_DAYS
find /backups -maxdepth 1 -type d -name '2*' -mtime "+$RETENTION_DAYS" -exec rm -rf {} \; 2>/dev/null || true
echo "✓ Local backup saved to $BACKUP_DIR"
# 5. Синхронизация с Яндекс.Диск
if command -v rclone > /dev/null 2>&1 && [ -n "${RCLONE_REMOTE:-}" ]; then
echo "→ Syncing to cloud: $RCLONE_REMOTE"
rclone sync /backups "$RCLONE_REMOTE" --progress 2>&1 || \
echo " ⚠ Cloud sync failed (check rclone config)"
echo "✓ Cloud sync complete"
fi
echo "=== Backup finished ==="
+43
View File
@@ -0,0 +1,43 @@
#!/bin/bash
# restore.sh — восстановление из бэкапа
# Использование: docker compose run --rm backup /opt/restore.sh [дата]
set -euo pipefail
BACKUP_DATE="${1:-latest}"
BACKUP_DIR="/backups/$BACKUP_DATE"
if [ ! -d "$BACKUP_DIR" ]; then
echo "Ошибка: бэкап $BACKUP_DIR не найден"
echo "Доступные бэкапы:"
ls -d /backups/2* 2>/dev/null || echo " (нет бэкапов)"
exit 1
fi
echo "=== Restore from $BACKUP_DIR ==="
# Восстановить БД
if [ -d "$BACKUP_DIR/db" ]; then
for dump in "$BACKUP_DIR/db"/*.dump; do
[ -f "$dump" ] || continue
db=$(basename "$dump" | sed 's/-.*//')
echo "→ Restoring database: $db"
PGPASSWORD="$DB_PASSWORD" pg_restore -h "$DB_HOST" -p "${DB_PORT:-5432}" \
-U "$DB_USER" -d "$db" --clean --if-exists "$dump" || \
echo " ⚠ Restore of $db had warnings (non-fatal)"
done
fi
# Распаковать файлы
if [ -d "$BACKUP_DIR/files" ]; then
for archive in "$BACKUP_DIR/files"/*.tar.gz; do
[ -f "$archive" ] || continue
name=$(basename "$archive" | sed 's/-.*//')
target="/data/$name"
echo "→ Extracting $name to $target"
mkdir -p "$target"
tar -xzf "$archive" -C "$target" || true
done
fi
echo "=== Restore completed ==="
echo "При необходимости перезапусти сервисы: docker compose restart"
+3 -13
View File
@@ -1,20 +1,10 @@
FROM certbot/certbot
# Проверяем наличие crond (используем command -v вместо which)
RUN if ! command -v crond > /dev/null 2>&1; then \
echo "Cron not found. Installing cronie..."; \
apk add --no-cache cronie; \
else \
echo "Cron is already installed."; \
fi
RUN apk add --no-cache cronie docker-cli
# Создаем директории для конфигов
RUN mkdir -p /etc/letsencrypt/config
# Копируем конфигурационные файлы
COPY scripts/ /opt/
RUN chmod +x /opt/*.sh
# Устанавливаем права
RUN chmod +x /opt/*
ENTRYPOINT ["/opt/init-certbot.sh"]
ENTRYPOINT ["/opt/init-certbot.sh"]
+10
View File
@@ -0,0 +1,10 @@
yalarba.ru
www.yalarba.ru
valitovgaziz.ru
www.valitovgaziz.ru
easysite102.ru
www.easysite102.ru
begushiybashkir.ru
www.begushiybashkir.ru
xn--80abahjtcfl5d0a8di.xn--p1ai
www.xn--80abahjtcfl5d0a8di.xn--p1ai
+2 -1
View File
@@ -19,7 +19,8 @@ check_local_cert() {
fi
# Преобразуем дату истечения в UNIX-время
expiry_unix=$(date -d "$expiry_date" +%s)
# expiry_unix=$(date -d "$expiry_date" +%s)
expiry_unix=$(date -D "%b %d %H:%M:%S %Y %Z" -d "$expiry_date" +%s 2>/dev/null)
# Текущая дата в UNIX-времени
current_unix=$(date +%s)
+1 -1
View File
@@ -1 +1 @@
0 0 * * * root /opt/checkRenewCerts.sh > /proc/1/fd/1 2>&1
0 0 * * * /opt/renew-all.sh > /proc/1/fd/1 2>&1
+24 -61
View File
@@ -1,69 +1,32 @@
#!/bin/sh
# init-certbot.sh — точка входа certbot контейнера
set -e
# Проверяем наличие сертификатов для yalarba.ru
if [ ! -d "/etc/letsencrypt/live/yalarba.ru" ]; then
echo "Получаем новые сертификаты yalarba.ru ..."
certbot certonly --webroot \
--config /etc/letsencrypt/config/certbot.ini \
-w /var/www/certbot \
-d ${DOMAINS_yalarba}
fi
echo "=== Certbot init ==="
echo "сertificates for ${DOMAINS_yalarba} is ready"
# Получаем сертификаты для всех доменов из DOMAINS_* env
env | grep '^DOMAINS_' | grep -v '^ALL_DOMAINS' | sort | while IFS='=' read -r var_name domains; do
primary_domain=$(echo "$domains" | cut -d, -f1)
# Проверяем наличие сертификатов для valitovgaziz.ru
if [ ! -d "/etc/letsencrypt/live/valitovgaziz.ru" ]; then
echo "Получаем новые сертификаты valitovgaziz ..."
certbot certonly --webroot \
--config /etc/letsencrypt/config/certbot.ini \
-w /var/www/certbot \
-d ${DOMAINS_valitovgaziz}
fi
if [ ! -d "/etc/letsencrypt/live/$primary_domain" ]; then
echo "→ Получаем сертификат для $primary_domain"
certbot certonly --webroot \
--config /etc/letsencrypt/config/certbot.ini \
-w /var/www/certbot \
-d "$domains"
echo "✓ Сертификат для $primary_domain получен"
else
echo "✓ Сертификат для $primary_domain уже существует"
fi
done
echo "сertificates for ${DOMAINS_valitovgaziz} is ready"
# Проверяем наличие сертификатов для easysite102.ru
if [ ! -d "/etc/letsencrypt/live/easysite102.ru" ]; then
echo "Получаем новые сертификаты easysite102.ru ..."
certbot certonly --webroot \
--config /etc/letsencrypt/config/certbot.ini \
-w /var/www/certbot \
-d ${DOMAINS_easysite102}
fi
echo "сertificates for ${DOMAINS_easysite102} is ready"
# Проверяем наличие сертификатов для бегущийбашкир.рф
if [ ! -d "/etc/letsencrypt/live/xn--80abahjtcfl5d0a8di.xn--p1ai" ]; then
echo "Получаем новые сертификаты xn--80abahjtcfl5d0a8di.xn--p1ai(бегущийбашкир.рф) ..."
certbot certonly --webroot \
--config /etc/letsencrypt/config/certbot.ini \
-w /var/www/certbot \
-d ${DOMAINS_begushiybashkir}
fi
echo "сertificates for ${DOMAINS_begushiybashkir} is ready"
# Проверяем наличие сертификатов для begushiybashkir.ru
if [ ! -d "/etc/letsencrypt/live/begushiybashkir.ru" ]; then
echo "Получаем новые сертификаты begushiybashkir.ru ..."
certbot certonly --webroot \
--config /etc/letsencrypt/config/certbot.ini \
-w /var/www/certbot \
-d ${DOMAINS_begushiybashkir_latin}
fi
echo "сertificates for ${DOMAINS_begushiybashkir_latin} is ready"
set -e # Завершаем работу, если любая команда вернёт ошибку
# Активируем сервис cron
/usr/sbin/crond -f &
crond -f &
# Копируем нашу собственную crontab таблицу
# Настраиваем cron для ежедневного обновления
cp /opt/crontab.txt /etc/crontabs/root
# Оставляем контейнер открытым
tail -f /dev/null
# Запускаем crond в фоне
crond -f &
echo "=== Init завершён, контейнер работает ==="
# Держим контейнер живым
tail -f /dev/null
+17
View File
@@ -0,0 +1,17 @@
#!/bin/sh
# renew-all.sh — единый скрипт обновления всех сертификатов
set -e
echo "=== Certbot renewal ==="
# Обновляем все сертификаты
certbot renew --webroot -w /var/www/certbot
# Перезагружаем nginx чтобы он подхватил новые сертификаты
if command -v docker > /dev/null 2>&1; then
echo "→ Перезагружаем nginx..."
docker exec nginx nginx -s reload 2>/dev/null || \
echo " (nginx reload не удался, возможно контейнер не запущен)"
fi
echo "=== Renewal завершён ==="
+131 -102
View File
@@ -10,19 +10,15 @@ services:
- ./certbot/config:/etc/letsencrypt/config
- certbot_data:/etc/letsencrypt
- certbot_www:/var/www/certbot
- /var/run/docker.sock:/var/run/docker.sock
env_file:
- .env
environment:
- EMAIL=${EMAIL}
- DOMAINS=${ALL_DOMAINS}
- STAGING=0
restart: unless-stopped
healthcheck:
test:
[
"CMD-SHELL",
"test -f /etc/letsencrypt/live/$$(echo $${DOMAINS} | cut -d',' -f1)/fullchain.pem || exit 1",
]
test: ["CMD-SHELL", "ls /etc/letsencrypt/live/*/fullchain.pem 2>/dev/null | head -1 | xargs test -f || exit 1"]
interval: 30s
timeout: 10s
retries: 3
@@ -43,32 +39,28 @@ services:
- certbot_data:/etc/letsencrypt
- certbot_www:/var/www/certbot
- ./stubSite:/usr/share/nginx/stub/html
- ./yalarba/serv_spa/spa/vue/dist:/usr/share/nginx/yalarba/html
- ./valitovgaziz/html:/usr/share/nginx/valitovgaziz/html
- ./BB/bbvue/dist:/usr/share/nginx/begushiybashkir/html
- analytics_logs:/var/log/analytics:ro
- ./nginx/conf.available:/etc/nginx/conf.available:ro
networks:
- web-network
- internal
- app-network
- bb-network
depends_on:
easysite:
condition: service_healthy
api_es:
condition: service_healthy
certbot:
condition: service_healthy
api_tp:
condition: service_healthy
api_bb:
condition: service_healthy
analytics:
condition: service_healthy
api_yal:
condition: service_healthy
yalarba:
condition: service_healthy
valitovgaziz:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost/health || exit 1"]
test: ["CMD", "wget", "--spider", "http://localhost/"]
interval: 30s
timeout: 10s
retries: 3
@@ -101,41 +93,24 @@ services:
retries: 3
start_period: 40s
# REST API app on Golang (Gorm, Chi) бизнес логика приложения yalarba.ru. Работает с БД на PostgresQL db:db_tp
api_tp:
# Vue 3 SPA для valitovgaziz.ru
valitovgaziz:
build:
context: ./yalarba/api_tp
context: ./valitovgaziz
dockerfile: Dockerfile
ports:
- "8888:8080"
container_name: api_tp
container_name: valitovgaziz
restart: unless-stopped
depends_on:
db:
condition: service_healthy
environment:
# Database connection settings
DB_HOST: db
DB_PORT: 5432
DB_USER: postgres
DB_PASSWORD: postgres
DB_NAME: mydb
APP_PORT: 8080
networks:
- app-network
- web-network
depends_on:
analytics:
condition: service_healthy
healthcheck:
test:
[
"CMD",
"wget",
"--no-verbose",
"--tries=1",
"--spider",
"http://localhost:8080/health",
]
test: ["CMD", "wget", "--spider", "http://localhost/"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
# PostgresQL DB база данных для храниния информации приложений Yalarba.ru && Easysite102.ru
db:
@@ -159,7 +134,7 @@ services:
timeout: 10s
retries: 5
# REST API on Golang (Gorm, Chi) логика обработки информации для сайта БегущийБашкир Работает с БД db_bb on PostgresQL
# REST API on Golang (Gorm, Chi) логика обработки информации для сайта БегущийБашкир
api_bb:
build:
context: ./BB/api_bb
@@ -169,22 +144,22 @@ services:
container_name: api_bb
restart: unless-stopped
depends_on:
db_bb:
db:
condition: service_healthy
env_file:
- ./BB/api_bb/.env
volumes:
- api_bb_uploads:/app/uploads
environment:
# Database connection settings
DB_HOST: db_bb
DB_HOST: db
DB_PORT: 5432
DB_USER: postgres
DB_PASSWORD: postgres
DB_NAME: bb_db
DB_SCHEMA: bb
APP_PORT: 8080
networks:
- bb-network
- app-network
healthcheck:
test:
[
@@ -199,31 +174,10 @@ services:
timeout: 10s
retries: 3
# PostgresQL DB база данных для работы сайта Бегущий Башкир
db_bb:
image: postgres:15-alpine
restart: unless-stopped
ports:
- "5433:5432"
container_name: db_bb
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: bb_db
volumes:
- db_bb_data:/var/lib/postgresql/data
networks:
- bb-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 30s
timeout: 10s
retries: 5
# SPA app прилжение выполнено на nuxt.js интерфейс для туристического бизнеса. Хранение информации в api_es REST API app
# SPA app прилжение выполнено на nuxt.js интерфейс для туристического бизнеса. Хранение информации в api_yal REST API app
easysite:
build:
context: ./yalarba/easySite/easySite
context: ./yalarba/easySite
dockerfile: Dockerfile
container_name: easysite
restart: unless-stopped
@@ -233,6 +187,7 @@ services:
NODE_ENV: production
HOST: 0.0.0.0
PORT: 3000
NUXT_PUBLIC_API_BASE: /api/v1
networks:
- web-network
- app-network
@@ -242,34 +197,6 @@ services:
timeout: 10s
retries: 3
# REST API приложение для easysite102.ru тут бизнес логика и система для обращения к PostgresQL БД (тоже сервис db:db_tp)
api_es:
build:
context: ./yalarba/api_es
dockerfile: Dockerfile
container_name: api_es
restart: unless-stopped
env_file:
- ./yalarba/api_es/.env
depends_on:
db:
condition: service_healthy
environment:
DB_HOST: db
DB_PORT: 5432
DB_USER: postgres
DB_PASSWORD: postgres
DB_NAME: mydb
APP_PORT: ${API_ES_APP_PORT}
networks:
- app-network
- web-network
healthcheck:
test: ["CMD", "wget", "--spider", "http://localhost:8088/health"]
interval: 30s
timeout: 10s
retries: 3
# REST API app on Golang для api_yal сервиса
api_yal:
build:
@@ -300,15 +227,119 @@ services:
retries: 3
start_period: 40s
# Nuxt 4 SPA для yalarba.ru
yalarba:
build:
context: ./yalarba/yalarba-nuxt
dockerfile: Dockerfile
container_name: yalarba
restart: unless-stopped
environment:
NODE_ENV: production
HOST: 0.0.0.0
PORT: 3000
NUXT_PUBLIC_API_BASE: /api/v1
NUXT_PUBLIC_APP_URL: https://yalarba.ru
networks:
- web-network
- app-network
healthcheck:
test: ["CMD", "wget", "--spider", "http://localhost:3000/api/health"]
interval: 30s
timeout: 10s
retries: 3
# ──────────────────────────────────────────────
# Gitea — self-hosted Git сервер + CI/CD
# ──────────────────────────────────────────────
gitea:
image: gitea/gitea:latest
container_name: gitea
restart: unless-stopped
ports:
- "3001:3000"
- "2222:22"
volumes:
- gitea_data:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
environment:
- USER_UID=1000
- USER_GID=1000
- GITEA__server__DOMAIN=git.yalarba.ru
- GITEA__server__SSH_DOMAIN=94.41.23.97
- GITEA__server__ROOT_URL=https://git.yalarba.ru
networks:
- web-network
- internal
healthcheck:
test: ["CMD", "wget", "--spider", "http://localhost:3000"]
interval: 30s
timeout: 10s
retries: 3
gitea-runner:
image: gitea/act_runner:latest
container_name: gitea-runner
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /home/gaziz/artefacts/tp:/home/gaziz/artefacts/tp
- gitea_runner:/data
environment:
- GITEA_INSTANCE_URL=http://gitea:3000
- GITEA_RUNNER_REGISTRATION_TOKEN=
depends_on:
gitea:
condition: service_healthy
networks:
- internal
# ──────────────────────────────────────────────
# Backup — ежедневные бэкапы БД + файлов → локально + Яндекс.Диск
# ──────────────────────────────────────────────
backup:
build:
context: ./backup
dockerfile: Dockerfile
container_name: backup
restart: unless-stopped
volumes:
- /var/backups/tp:/backups
- certbot_data:/data/certbot:ro
- api_bb_uploads:/data/uploads:ro
- analytics_data:/data/analytics:ro
- ./backup/rclone.conf:/root/.config/rclone/rclone.conf:ro
environment:
DB_HOST: db
DB_PORT: 5432
DB_USER: postgres
DB_PASSWORD: postgres
DB_NAMES: mydb,bb_db
RCLONE_REMOTE: "yadisk:tp-backups"
BACKUP_RETENTION_DAYS: 7
BACKUP_TIME: "0 3 * * *"
healthcheck:
test: ["CMD-SHELL", "pidof crond > /dev/null && ls /backups/ > /dev/null || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 15s
depends_on:
db:
condition: service_healthy
networks:
- app-network
volumes:
certbot_data: # volume для данных Certbot
certbot_www: # volume для данных Certbot
db_tp_data: # Volume для данных БД yalarba.ru
db_bb_data: # Volume для данных БД Бегущий башкир
api_bb_uploads: # Volume для загружаемых файлов бегущий башкир
analytics_logs: # Volume для логов аналитики
analytics_data: # Volume для данных аналитики
gitea_data: # Volume для Gitea
gitea_runner: # Volume для Gitea Runner
networks:
web-network:
@@ -317,8 +348,6 @@ networks:
driver: bridge
app-network:
driver: bridge
bb-network:
driver: bridge
# Эта опция автоматически удаляет orphans (Не используемые контейнеры)
x-remove-orphans: true
+474
View File
@@ -0,0 +1,474 @@
#!/bin/bash
# generate-configs.sh — генератор конфигов из sites.yml
# Генерирует: nginx-http.conf, nginx-ssl.conf, certbot/domains.txt, обновляет .env
set -euo pipefail
DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$DIR"
SITES_YML="$DIR/sites.yml"
NGINX_DIR="$DIR/nginx"
ENV_FILE="$DIR/.env"
if [ ! -f "$SITES_YML" ]; then
echo "Ошибка: $SITES_YML не найден"
exit 1
fi
echo "=== Генерация конфигов из sites.yml ==="
# Используем python3 с quoted heredoc — предотвращает интерпретацию $ переменных bash
python3 - "$DIR" "$NGINX_DIR" "$ENV_FILE" << 'PYEOF'
import yaml, os, sys
BASE_DIR = sys.argv[1]
NGINX_DIR = sys.argv[2]
ENV_FILE = sys.argv[3]
SITES_YML = os.path.join(BASE_DIR, "sites.yml")
with open(SITES_YML) as f:
data = yaml.safe_load(f)
sites = data.get("sites", {})
if not sites:
print("Ошибка: в sites.yml нет сайтов")
sys.exit(1)
# собираем данные
all_domains = []
env_domains = {}
site_list = []
for name, cfg in sites.items():
domain = cfg["domain"]
aliases = cfg.get("aliases", [])
all_domains.append(domain)
all_domains.extend(aliases)
env_key = f"DOMAINS_{name}"
env_val = ",".join([domain] + aliases)
env_domains[env_key] = env_val
site_list.append({
"name": name,
"domain": domain,
"aliases": aliases,
"type": cfg.get("type", "upstream"),
"upstream": cfg.get("upstream", ""),
"root": cfg.get("root", ""),
"api": cfg.get("api", {}),
})
env_domains["ALL_DOMAINS"] = ",".join(all_domains)
def all_server_names():
"""Возвращает строку со всеми доменами и алиасами через пробел"""
parts = []
for s in site_list:
parts.append(s["domain"])
parts.extend(s["aliases"])
return " ".join(parts)
def all_server_names_multiline():
"""Возвращает строку с переносами для nginx server_name"""
lines = []
for s in site_list:
lines.append(s["domain"])
for a in s["aliases"]:
lines.append(a)
return " \\\n ".join(lines)
# ──────────────────────────────────────────────
# 2. Генерация nginx-http.conf
# ──────────────────────────────────────────────
http_conf = f"""# Автоматически сгенерировано generate-configs.sh — не редактировать вручную
# HTTP-only конфигурация (работает когда нет сертификатов)
server {{
listen 80;
server_name {all_server_names_multiline()};
location / {{
root /usr/share/nginx/stub/html;
index index.html;
}}
location /.well-known/acme-challenge/ {{
root /var/www/certbot;
}}
}}
# Блок для HTTPS → HTTP редиректа (порт 443)
server {{
listen 443 ssl;
server_name {all_server_names_multiline()};
ssl_certificate /etc/nginx/ssl/dummy.crt;
ssl_certificate_key /etc/nginx/ssl/dummy.key;
return 301 http://$host$request_uri;
}}
"""
http_conf_path = os.path.join(NGINX_DIR, "nginx-http.conf")
with open(http_conf_path, "w") as f:
f.write(http_conf.lstrip())
print(f" ✓ {http_conf_path}")
# ──────────────────────────────────────────────
# 3. Генерация nginx-ssl.conf
# ──────────────────────────────────────────────
ssl_server_blocks = []
for s in site_list:
server_names = " ".join([s["domain"]] + s["aliases"])
block = f"""
server {{
listen 443 ssl;
server_name {server_names};
ssl_certificate /etc/letsencrypt/live/{s["domain"]}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/{s["domain"]}/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
"""
if s["type"] == "upstream":
block += f"""
location / {{
proxy_pass {s["upstream"]};
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
proxy_connect_timeout 600;
proxy_send_timeout 600;
proxy_read_timeout 600;
}}
"""
elif s["type"] == "static":
block += f"""
location / {{
root {s["root"]};
index index.html;
try_files $uri $uri/ /index.html;
}}
"""
# API routes
for path, target in s["api"].items():
cors_block = ""
if "/api/" in path:
cors_block = """
if ($request_method = OPTIONS) {
add_header 'Access-Control-Allow-Origin' "$http_origin";
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE';
add_header 'Access-Control-Allow-Headers' 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Length' 0;
add_header 'Content-Type' 'text/plain charset=UTF-8';
return 204;
}
"""
block += f"""
location {path} {{
proxy_pass {target};
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
proxy_connect_timeout 600;
proxy_send_timeout 600;
proxy_read_timeout 600;
{cors_block}
}}
"""
if s["type"] == "static":
block += f"""
location /uploads/ {{
alias /uploads/;
expires 1y;
add_header Cache-Control "public, immutable";
}}
"""
block += "}"
ssl_server_blocks.append(block)
ssl_conf = f"""# Автоматически сгенерировано generate-configs.sh — не редактировать вручную
# Полная HTTPS конфигурация
# --- HTTP → HTTPS редирект ---
server {{
listen 80;
server_name {all_server_names()};
location /.well-known/acme-challenge/ {{
root /var/www/certbot;
}}
location /uploads/ {{
alias /uploads/;
expires 1y;
add_header Cache-Control "public, immutable";
}}
location / {{
return 301 https://$host$request_uri;
}}
}}
# --- HTTPS серверные блоки ---
{''.join(ssl_server_blocks)}
"""
ssl_conf_path = os.path.join(NGINX_DIR, "nginx-ssl.conf")
with open(ssl_conf_path, "w") as f:
f.write(ssl_conf.lstrip())
print(f" ✓ {ssl_conf_path}")
# ──────────────────────────────────────────────
# 4. Генерация per-domain конфигов (conf.available/)
# ──────────────────────────────────────────────
CONF_AVAILABLE = os.path.join(NGINX_DIR, "conf.available")
os.makedirs(CONF_AVAILABLE, exist_ok=True)
# 00-http.conf — базовый HTTP catch-all (всегда активен)
base_http = f"""# Автоматически сгенерировано generate-configs.sh
server {{
listen 80 default_server;
server_name _;
location / {{
root /usr/share/nginx/stub/html;
index index.html;
}}
location /.well-known/acme-challenge/ {{
root /var/www/certbot;
}}
}}
"""
path = os.path.join(CONF_AVAILABLE, "00-http.conf")
with open(path, "w") as f:
f.write(base_http.lstrip())
print(f" ✓ conf.available/00-http.conf")
# per-domain: SSL + HTTP fallback
ORDER = ["10", "20", "30", "40", "50", "60", "70", "80", "90"]
for idx, s in enumerate(site_list):
prefix = ORDER[idx] if idx < len(ORDER) else f"{90 + idx}"
safe_name = s["name"]
server_names = " ".join([s["domain"]] + s["aliases"])
# --- SSL variant ---
ssl_block = f"""# CERT_DOMAIN={s["domain"]}
# Автоматически сгенерировано generate-configs.sh
server {{
listen 443 ssl;
server_name {server_names};
ssl_certificate /etc/letsencrypt/live/{s["domain"]}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/{s["domain"]}/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
"""
if s["type"] == "upstream":
ssl_block += f"""
location / {{
proxy_pass {s["upstream"]};
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
proxy_connect_timeout 600;
proxy_send_timeout 600;
proxy_read_timeout 600;
}}
"""
elif s["type"] == "static":
ssl_block += f"""
location / {{
root {s["root"]};
index index.html;
try_files $uri $uri/ /index.html;
}}
"""
for path, target in s["api"].items():
cors = ""
if "/api/" in path:
cors = """
if ($request_method = OPTIONS) {
add_header 'Access-Control-Allow-Origin' "$http_origin";
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE';
add_header 'Access-Control-Allow-Headers' 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Length' 0;
add_header 'Content-Type' 'text/plain charset=UTF-8';
return 204;
}
"""
ssl_block += f"""
location {path} {{
proxy_pass {target};
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
proxy_connect_timeout 600;
proxy_send_timeout 600;
proxy_read_timeout 600;
{cors}
}}
"""
if s["type"] == "static":
ssl_block += f"""
location /uploads/ {{
alias /uploads/;
expires 1y;
add_header Cache-Control "public, immutable";
}}
"""
ssl_block += "}"
ssl_path = os.path.join(CONF_AVAILABLE, f"{prefix}-{safe_name}.ssl.conf")
with open(ssl_path, "w") as f:
f.write(ssl_block.lstrip())
# --- HTTP fallback variant ---
http_block = f"""# HTTP fallback for {s["domain"]} (no SSL cert)
server {{
listen 80;
server_name {server_names};
"""
if s["type"] == "upstream":
http_block += f"""
location / {{
proxy_pass {s["upstream"]};
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 600;
proxy_send_timeout 600;
proxy_read_timeout 600;
}}
"""
elif s["type"] == "static":
http_block += f"""
location / {{
root {s["root"]};
index index.html;
try_files $uri $uri/ /index.html;
}}
"""
for path, target in s["api"].items():
cors = ""
if "/api/" in path:
cors = """
if ($request_method = OPTIONS) {
add_header 'Access-Control-Allow-Origin' "$http_origin";
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE';
add_header 'Access-Control-Allow-Headers' 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Length' 0;
add_header 'Content-Type' 'text/plain charset=UTF-8';
return 204;
}
"""
http_block += f"""
location {path} {{
proxy_pass {target};
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 600;
proxy_send_timeout 600;
proxy_read_timeout 600;
{cors}
}}
"""
if s["type"] == "static":
http_block += f"""
location /uploads/ {{
alias /uploads/;
expires 1y;
add_header Cache-Control "public, immutable";
}}
"""
http_block += "}"
http_path = os.path.join(CONF_AVAILABLE, f"{prefix}-{safe_name}.http.conf")
with open(http_path, "w") as f:
f.write(http_block.lstrip())
print(f" ✓ conf.available/{prefix}-{safe_name}.ssl.conf + .http.conf")
# ──────────────────────────────────────────────
# 5. Генерация certbot/domains.txt
# ──────────────────────────────────────────────
domains_txt_path = os.path.join(BASE_DIR, "certbot", "domains.txt")
with open(domains_txt_path, "w") as f:
for d in all_domains:
f.write(d + "\n")
print(f" ✓ {domains_txt_path}")
# ──────────────────────────────────────────────
# 6. Обновление .env
# ──────────────────────────────────────────────
if os.path.exists(ENV_FILE):
with open(ENV_FILE) as f:
env_lines = f.readlines()
else:
env_lines = []
new_env = []
for line in env_lines:
stripped = line.strip()
if stripped.startswith("DOMAINS_") or stripped.startswith("ALL_DOMAINS") or "CERTBOT NGINX VARIABLES" in stripped:
continue
new_env.append(line)
# удаляем пустые строки в начале
while new_env and not new_env[0].strip():
new_env.pop(0)
domain_keys = {k: v for k, v in env_domains.items()}
insert_idx = None
for i, line in enumerate(new_env):
if line.strip().startswith("EMAIL="):
insert_idx = i + 1
break
env_header = "#CERTBOT NGINX VARIABLES — авто-сгенерировано, не редактировать вручную\n"
domain_lines = [f"{k}={v}\n" for k, v in sorted(domain_keys.items())]
if insert_idx is not None:
new_env.insert(insert_idx, env_header)
for dl in reversed(domain_lines):
new_env.insert(insert_idx + 1, dl)
else:
new_env = [env_header] + domain_lines + new_env
with open(ENV_FILE, "w") as f:
f.writelines(new_env)
print(f" ✓ {ENV_FILE} (обновлён)")
print()
print("=== Генерация завершена ===")
print(f"Сгенерировано {len(site_list)} сайтов:")
for s in site_list:
print(f" • {s['domain']} ({s['type']})")
print()
print("Не забудь перезапустить nginx: docker compose restart nginx")
PYEOF
+10 -21
View File
@@ -1,28 +1,17 @@
FROM nginx:alpine
# Установка зависимостей
RUN apk add --no-cache bash openssl
# Создание директории для сертификатов
RUN mkdir -p /etc/nginx/ssl
# dummy сертификаты для nginx (нужны чтобы nginx стартовал с любым конфигом)
RUN mkdir -p /etc/nginx/ssl && \
openssl req -x509 -nodes -days 365 \
-newkey rsa:2048 \
-keyout /etc/nginx/ssl/dummy.key \
-out /etc/nginx/ssl/dummy.crt \
-subj "/C=US/ST=State/L=City/O=Organization/CN=localhost"
# Генерация самоподписанных сертификатов (действительны 365 дней)
RUN openssl req -x509 -nodes -days 365 \
-newkey rsa:2048 \
-keyout /etc/nginx/ssl/dummy.key \
-out /etc/nginx/ssl/dummy.crt \
-subj "/C=US/ST=State/L=City/O=Organization/CN=localhost"
RUN mkdir -p /var/www/certbot /etc/nginx/conf.d /etc/nginx/conf.available
# Копируем обе конфигурации
COPY nginx-http.conf /etc/nginx/nginx-http.conf
COPY nginx-ssl.conf /etc/nginx/nginx-ssl.conf
# Создаем симлинк по умолчанию на HTTP конфиг
RUN ln -sf /etc/nginx/nginx-http.conf /etc/nginx/conf.d/default.conf
# Скрипт для проверки сертификатов и переключения конфига
COPY switch-config.sh /docker-entrypoint.d/switch-config.sh
# per-domain entrypoint для проверки сертификатов
COPY entrypoint.sh /docker-entrypoint.d/switch-config.sh
RUN chmod +x /docker-entrypoint.d/switch-config.sh
# Создаем необходимые директории
RUN mkdir -p /var/www/certbot
+14
View File
@@ -0,0 +1,14 @@
# Автоматически сгенерировано generate-configs.sh
server {
listen 80 default_server;
server_name _;
location / {
root /usr/share/nginx/stub/html;
index index.html;
}
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
}
@@ -0,0 +1,38 @@
# HTTP fallback for yalarba.ru (no SSL cert)
server {
listen 80;
server_name yalarba.ru www.yalarba.ru;
location / {
proxy_pass http://yalarba:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 600;
proxy_send_timeout 600;
proxy_read_timeout 600;
}
location /api/v1/ {
proxy_pass http://api_yal:8787;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 600;
proxy_send_timeout 600;
proxy_read_timeout 600;
if ($request_method = OPTIONS) {
add_header 'Access-Control-Allow-Origin' "$http_origin";
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE';
add_header 'Access-Control-Allow-Headers' 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Length' 0;
add_header 'Content-Type' 'text/plain charset=UTF-8';
return 204;
}
}
}
@@ -0,0 +1,48 @@
# CERT_DOMAIN=yalarba.ru
# Автоматически сгенерировано generate-configs.sh
server {
listen 443 ssl;
server_name yalarba.ru www.yalarba.ru;
ssl_certificate /etc/letsencrypt/live/yalarba.ru/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yalarba.ru/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
location / {
proxy_pass http://yalarba:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
proxy_connect_timeout 600;
proxy_send_timeout 600;
proxy_read_timeout 600;
}
location /api/v1/ {
proxy_pass http://api_yal:8787;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
proxy_connect_timeout 600;
proxy_send_timeout 600;
proxy_read_timeout 600;
if ($request_method = OPTIONS) {
add_header 'Access-Control-Allow-Origin' "$http_origin";
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE';
add_header 'Access-Control-Allow-Headers' 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Length' 0;
add_header 'Content-Type' 'text/plain charset=UTF-8';
return 204;
}
}
}
@@ -0,0 +1,38 @@
# HTTP fallback for valitovgaziz.ru (no SSL cert)
server {
listen 80;
server_name valitovgaziz.ru www.valitovgaziz.ru;
location / {
proxy_pass http://valitovgaziz/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 600;
proxy_send_timeout 600;
proxy_read_timeout 600;
}
location /api/ {
proxy_pass http://analytics:3000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 600;
proxy_send_timeout 600;
proxy_read_timeout 600;
if ($request_method = OPTIONS) {
add_header 'Access-Control-Allow-Origin' "$http_origin";
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE';
add_header 'Access-Control-Allow-Headers' 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Length' 0;
add_header 'Content-Type' 'text/plain charset=UTF-8';
return 204;
}
}
}
@@ -0,0 +1,48 @@
# CERT_DOMAIN=valitovgaziz.ru
# Автоматически сгенерировано generate-configs.sh
server {
listen 443 ssl;
server_name valitovgaziz.ru www.valitovgaziz.ru;
ssl_certificate /etc/letsencrypt/live/valitovgaziz.ru/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/valitovgaziz.ru/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
location / {
proxy_pass http://valitovgaziz/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
proxy_connect_timeout 600;
proxy_send_timeout 600;
proxy_read_timeout 600;
}
location /api/ {
proxy_pass http://analytics:3000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
proxy_connect_timeout 600;
proxy_send_timeout 600;
proxy_read_timeout 600;
if ($request_method = OPTIONS) {
add_header 'Access-Control-Allow-Origin' "$http_origin";
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE';
add_header 'Access-Control-Allow-Headers' 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Length' 0;
add_header 'Content-Type' 'text/plain charset=UTF-8';
return 204;
}
}
}
@@ -0,0 +1,38 @@
# HTTP fallback for easysite102.ru (no SSL cert)
server {
listen 80;
server_name easysite102.ru www.easysite102.ru;
location / {
proxy_pass http://easysite:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 600;
proxy_send_timeout 600;
proxy_read_timeout 600;
}
location /api/v1/ {
proxy_pass http://api_yal:8787;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 600;
proxy_send_timeout 600;
proxy_read_timeout 600;
if ($request_method = OPTIONS) {
add_header 'Access-Control-Allow-Origin' "$http_origin";
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE';
add_header 'Access-Control-Allow-Headers' 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Length' 0;
add_header 'Content-Type' 'text/plain charset=UTF-8';
return 204;
}
}
}
@@ -0,0 +1,48 @@
# CERT_DOMAIN=easysite102.ru
# Автоматически сгенерировано generate-configs.sh
server {
listen 443 ssl;
server_name easysite102.ru www.easysite102.ru;
ssl_certificate /etc/letsencrypt/live/easysite102.ru/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/easysite102.ru/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
location / {
proxy_pass http://easysite:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
proxy_connect_timeout 600;
proxy_send_timeout 600;
proxy_read_timeout 600;
}
location /api/v1/ {
proxy_pass http://api_yal:8787;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
proxy_connect_timeout 600;
proxy_send_timeout 600;
proxy_read_timeout 600;
if ($request_method = OPTIONS) {
add_header 'Access-Control-Allow-Origin' "$http_origin";
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE';
add_header 'Access-Control-Allow-Headers' 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Length' 0;
add_header 'Content-Type' 'text/plain charset=UTF-8';
return 204;
}
}
}
@@ -0,0 +1,39 @@
# HTTP fallback for begushiybashkir.ru (no SSL cert)
server {
listen 80;
server_name begushiybashkir.ru www.begushiybashkir.ru;
location / {
root /usr/share/nginx/begushiybashkir/html;
index index.html;
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://api_bb:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 600;
proxy_send_timeout 600;
proxy_read_timeout 600;
if ($request_method = OPTIONS) {
add_header 'Access-Control-Allow-Origin' "$http_origin";
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE';
add_header 'Access-Control-Allow-Headers' 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Length' 0;
add_header 'Content-Type' 'text/plain charset=UTF-8';
return 204;
}
}
location /uploads/ {
alias /uploads/;
expires 1y;
add_header Cache-Control "public, immutable";
}
}
@@ -0,0 +1,48 @@
# CERT_DOMAIN=begushiybashkir.ru
# Автоматически сгенерировано generate-configs.sh
server {
listen 443 ssl;
server_name begushiybashkir.ru www.begushiybashkir.ru;
ssl_certificate /etc/letsencrypt/live/begushiybashkir.ru/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/begushiybashkir.ru/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
location / {
root /usr/share/nginx/begushiybashkir/html;
index index.html;
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://api_bb:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
proxy_connect_timeout 600;
proxy_send_timeout 600;
proxy_read_timeout 600;
if ($request_method = OPTIONS) {
add_header 'Access-Control-Allow-Origin' "$http_origin";
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE';
add_header 'Access-Control-Allow-Headers' 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Length' 0;
add_header 'Content-Type' 'text/plain charset=UTF-8';
return 204;
}
}
location /uploads/ {
alias /uploads/;
expires 1y;
add_header Cache-Control "public, immutable";
}
}
@@ -0,0 +1,39 @@
# HTTP fallback for xn--80abahjtcfl5d0a8di.xn--p1ai (no SSL cert)
server {
listen 80;
server_name xn--80abahjtcfl5d0a8di.xn--p1ai www.xn--80abahjtcfl5d0a8di.xn--p1ai;
location / {
root /usr/share/nginx/begushiybashkir/html;
index index.html;
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://api_bb:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 600;
proxy_send_timeout 600;
proxy_read_timeout 600;
if ($request_method = OPTIONS) {
add_header 'Access-Control-Allow-Origin' "$http_origin";
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE';
add_header 'Access-Control-Allow-Headers' 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Length' 0;
add_header 'Content-Type' 'text/plain charset=UTF-8';
return 204;
}
}
location /uploads/ {
alias /uploads/;
expires 1y;
add_header Cache-Control "public, immutable";
}
}
@@ -0,0 +1,48 @@
# CERT_DOMAIN=xn--80abahjtcfl5d0a8di.xn--p1ai
# Автоматически сгенерировано generate-configs.sh
server {
listen 443 ssl;
server_name xn--80abahjtcfl5d0a8di.xn--p1ai www.xn--80abahjtcfl5d0a8di.xn--p1ai;
ssl_certificate /etc/letsencrypt/live/xn--80abahjtcfl5d0a8di.xn--p1ai/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/xn--80abahjtcfl5d0a8di.xn--p1ai/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
location / {
root /usr/share/nginx/begushiybashkir/html;
index index.html;
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://api_bb:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
proxy_connect_timeout 600;
proxy_send_timeout 600;
proxy_read_timeout 600;
if ($request_method = OPTIONS) {
add_header 'Access-Control-Allow-Origin' "$http_origin";
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE';
add_header 'Access-Control-Allow-Headers' 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Length' 0;
add_header 'Content-Type' 'text/plain charset=UTF-8';
return 204;
}
}
location /uploads/ {
alias /uploads/;
expires 1y;
add_header Cache-Control "public, immutable";
}
}
+7 -7
View File
@@ -41,7 +41,7 @@
│ • certbot - SSL сертификаты │
│ • analytics - Статистика (Node.js) │
│ • api_tp - API yalarba.ru (Go) │
│ • api_es - API easysite102.ru (Go) │
│ • api_yal - API easysite102.ru (Go) │
│ • api_bb - API Бегущий Башкир (Go) │
│ • easysite - SPA (Nuxt.js) │
│ • db - PostgreSQL (yalarba/easy) │
@@ -72,9 +72,9 @@
| Домен | Тип | Backend сервис | Путь на диске |
|-------|-----|----------------|---------------|
| `yalarba.ru` | SPA (Vue) | `api_tp:8080` | `/usr/share/nginx/yalarba/html` |
| `yalarba.ru` | Nuxt 4 SSR | `yalarba:3000` + `api_yal:8787` | Прокси |
| `valitovgaziz.ru` | Статический сайт | - | `/usr/share/nginx/valitovgaziz/html` |
| `easysite102.ru` | SPA (Nuxt.js) | `easysite:3000` + `api_es:8088` | Прокси |
| `easysite102.ru` | SPA (Nuxt.js) | `easysite:3000` + `api_yal:8787` | Прокси |
| `begushiybashkir.ru` | SPA (Vue) | `api_bb:8080` | `/usr/share/nginx/begushiybashkir/html` |
| `xn--80abahjtcfl5d0a8di.xn--p1ai` | Альтернативный домен для Бегущий Башкир | `api_bb:8080` | `/usr/share/nginx/begushiybashkir/html` |
@@ -118,7 +118,7 @@
```
EMAIL=admin@example.com # Для Let's Encrypt
ALL_DOMAINS=yalarba.ru,valitovgaziz.ru... # Все домены для SSL
API_ES_APP_PORT=8088 # Порт API easysite
# API_ES убран, используется api_yal:8787
```
### Сервисные
@@ -141,14 +141,14 @@ STAGING=0 # 1 для тестового режима Let's Encrypt
| certbot | Проверка файла сертификата | - | 30s |
| analytics | `http://localhost:3000/health` | 3000 | 30s |
| api_tp | `http://localhost:8080/health` | 8080 | 30s |
| api_es | `http://localhost:8088/health` | 8088 | 30s |
| api_yal | `http://localhost:8787/health` | 8787 | 30s |
| api_bb | `http://localhost:8080/api/health` | 8080 | 30s |
| easysite | `http://localhost:3000/api/health` | 3000 | 30s |
| db, db_bb | `pg_isready -U postgres` | 5432 | 30s |
### Зависимости запуска
Nginx запускается только после подтверждения здоровья:
- `easysite`, `api_es`, `certbot`, `api_tp`, `api_bb`, `analytics`
- `easysite`, `api_yal`, `certbot`, `api_tp`, `api_bb`, `analytics`
## Волумы
@@ -165,7 +165,7 @@ Nginx запускается только после подтверждения
### Монтирование статических файлов
```
./stubSite → /usr/share/nginx/stub/html
./yalarba/serv_spa/spa/vue/dist → /usr/share/nginx/yalarba/html
# удалено: serv_spa больше не используется, yalarba работает через Nuxt SSR (yalarba-nuxt)
./valitovgaziz/html → /usr/share/nginx/valitovgaziz/html
./BB/bbvue/dist → /usr/share/nginx/begushiybashkir/html
```
+37
View File
@@ -0,0 +1,37 @@
#!/bin/bash
# entrypoint.sh — per-domain HTTPS переключение
# Для каждого домена проверяет сертификат и активирует SSL или HTTP конфиг
set -euo pipefail
CONF_AVAILABLE="/etc/nginx/conf.available"
CONF_D="/etc/nginx/conf.d"
CERT_DIR="/etc/letsencrypt/live"
rm -f "$CONF_D"/*.conf
# базовый HTTP (ACME challenge, catch-all redirect)
if [ -f "$CONF_AVAILABLE/00-http.conf" ]; then
ln -sf "$CONF_AVAILABLE/00-http.conf" "$CONF_D/00-http.conf"
fi
# per-domain конфиги
shopt -s nullglob
for ssl_conf in "$CONF_AVAILABLE"/*.ssl.conf; do
base="$(basename "$ssl_conf" .ssl.conf)"
http_conf="$CONF_AVAILABLE/$base.http.conf"
# CERT_DOMAIN в первой строке: # CERT_DOMAIN=example.ru
cert_domain="$(head -1 "$ssl_conf" | sed -n 's/.*# CERT_DOMAIN=\(.*\)/\1/p')" || true
if [ -n "$cert_domain" ] && [ -f "$CERT_DIR/$cert_domain/fullchain.pem" ]; then
ln -sf "$ssl_conf" "$CONF_D/$base.ssl.conf"
echo "$base → HTTPS ($cert_domain)"
elif [ -f "$http_conf" ]; then
ln -sf "$http_conf" "$CONF_D/$base.http.conf"
echo "$base → HTTP (no cert for $cert_domain)"
fi
done
echo "---"
ls -la "$CONF_D/" | grep -v '^total'
nginx -t
+18 -9
View File
@@ -1,16 +1,18 @@
# Автоматически сгенерировано generate-configs.sh — не редактировать вручную
# HTTP-only конфигурация (работает когда нет сертификатов)
server {
listen 80;
server_name yalarba.ru \
www.yalarba.ru \
easysite102.ru \
www.easysite102.ru \
valitovgaziz.ru \
www.valitovgaziz.ru \
xn--80abahjtcfl5d0a8di.xn--p1ai \
www.xn--80abahjtcfl5d0a8di.xn--p1ai \
easysite102.ru \
www.easysite102.ru \
begushiybashkir.ru \
www.begushiybashkir.ru \
auth.yalarba.ru;
xn--80abahjtcfl5d0a8di.xn--p1ai \
www.xn--80abahjtcfl5d0a8di.xn--p1ai;
location / {
root /usr/share/nginx/stub/html;
@@ -25,12 +27,19 @@ server {
# Блок для HTTPS → HTTP редиректа (порт 443)
server {
listen 443 ssl;
server_name yalarba.ru www.yalarba.ru easysite102.ru www.easysite102.ru valitovgaziz.ru www.valitovgaziz.ru xn--80abahjtcfl5d0a8di.xn--p1ai www.xn--80abahjtcfl5d0a8di.xn--p1ai begushiybashkir.ru www.begushiybashkir.ru;
server_name yalarba.ru \
www.yalarba.ru \
valitovgaziz.ru \
www.valitovgaziz.ru \
easysite102.ru \
www.easysite102.ru \
begushiybashkir.ru \
www.begushiybashkir.ru \
xn--80abahjtcfl5d0a8di.xn--p1ai \
www.xn--80abahjtcfl5d0a8di.xn--p1ai;
# Указание пустых сертификатов (обязательно для запуска Nginx)
ssl_certificate /etc/nginx/ssl/dummy.crt;
ssl_certificate_key /etc/nginx/ssl/dummy.key;
# Редирект всех HTTPS-запросов на HTTP
return 301 http://$host$request_uri;
}
}
+98 -283
View File
@@ -1,341 +1,146 @@
# ================================================
# КОНФИГУРАЦИЯ NGINX С ПОДДЕРЖКОЙ SSL
# Основные задачи:
# 1. Перенаправление HTTP → HTTPS
# 2. Обслуживание статических файлов
# 3. Проксирование к backend сервисам
# 4. Поддержка нескольких доменов
# ================================================
# Автоматически сгенерировано generate-configs.sh — не редактировать вручную
# Полная HTTPS конфигурация
# ================================================
# БЛОК 1: HTTP СЕРВЕР (ПОРТ 80)
# ================================================
# --- HTTP → HTTPS редирект ---
server {
# Прослушивание порта 80 для всех входящих HTTP соединений
listen 80;
# Список доменов, которые обслуживает этот сервер
# Все запросы к этим доменам по HTTP будут обработаны здесь
server_name yalarba.ru www.yalarba.ru
valitovgaziz.ru www.valitovgaziz.ru
easysite102.ru www.easysite102.ru
begushiybashkir.ru
xn--80abahjtcfl5d0a8di.xn--p1ai; # Punycode для IDN домена
# ============================================
# ЛОКАЦИЯ: Проверочные файлы для Certbot
# ============================================
# Этот блок КРИТИЧЕСКИ ВАЖЕН для получения SSL сертификатов
# Certbot (Let's Encrypt) размещает здесь временные файлы
# для подтверждения владения доменом
server_name yalarba.ru www.yalarba.ru valitovgaziz.ru www.valitovgaziz.ru easysite102.ru www.easysite102.ru begushiybashkir.ru www.begushiybashkir.ru xn--80abahjtcfl5d0a8di.xn--p1ai www.xn--80abahjtcfl5d0a8di.xn--p1ai;
location /.well-known/acme-challenge/ {
# Директория, где Certbot хранит проверочные файлы
root /var/www/certbot;
# Дополнительные настройки не нужны - nginx просто отдает файлы
}
# ============================================
# ЛОКАЦИЯ: Основное перенаправление
# ============================================
# Все HTTP запросы перенаправляются на HTTPS
# Это обеспечивает безопасность и правильную SEO-практику
location / {
# 301 - постоянный редирект (лучше для SEO, кэшируется браузерами)
# https://$host$request_uri - сохраняет домен и полный путь запроса
return 301 https://$host$request_uri;
# Пример:
# HTTP: http://example.com/page?param=1
# ↓ перенаправление ↓
# HTTPS: https://example.com/page?param=1
}
# ============================================
# ЛОКАЦИЯ: Загруженные файлы
# ============================================
# Обслуживание статических файлов (загрузок) по HTTP
# Может быть полезно для прямых ссылок или кэширования
location /uploads/ {
# Псевдоним пути - запросы к /uploads/ обслуживаются из /uploads/ на диске
alias /uploads/;
# Кэширование в браузере на 1 год
expires 1y;
# Заголовки кэширования:
# "public" - может кэшироваться прокси-серверами
# "immutable" - файлы никогда не меняются, браузер не проверяет обновления
add_header Cache-Control "public, immutable";
# Если файл не найден - вернуть 404 ошибку
try_files $uri =404;
}
location / {
return 301 https://$host$request_uri;
}
}
# ================================================
# БЛОК 2: HTTPS СЕРВЕР ДЛЯ YALARBA.RU
# ================================================
# --- HTTPS серверные блоки ---
server {
# Прослушивание порта 443 с SSL/TLS шифрованием
listen 443 ssl;
# Домены для этого сервера
server_name yalarba.ru www.yalarba.ru;
# ============================================
# НАСТРОЙКИ SSL СЕРТИФИКАТОВ
# ============================================
# Пути к SSL сертификатам, сгенерированным Certbot
ssl_certificate /etc/letsencrypt/live/yalarba.ru/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yalarba.ru/privkey.pem;
# ============================================
# НАСТРОЙКИ БЕЗОПАСНОСТИ SSL
# ============================================
# Разрешенные протоколы - только современные безопасные версии
ssl_protocols TLSv1.2 TLSv1.3;
# Сервер выбирает шифры (не клиент)
ssl_prefer_server_ciphers on;
# Список безопасных шифров
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
# ============================================
# ЛОКАЦИЯ: Корневая (SPA приложение)
# ============================================
location / {
# Директория со скомпилированным Vue/React приложением
root /usr/share/nginx/yalarba/html;
# Файл по умолчанию
index index.html;
# Логика SPA роутинга:
# 1. Пробуем найти точный файл ($uri)
# 2. Пробуем найти директорию ($uri/)
# 3. Если не нашли - отдаем index.html
# Это позволяет клиентскому роутингу работать корректно
try_files $uri $uri/ /index.html;
}
# ============================================
# ЛОКАЦИЯ: REST API Backend
# ============================================
location /api/ {
# Проксирование всех запросов к API на Golang сервис
proxy_pass http://api_tp/; # Контейнер Docker
# Передача оригинальных заголовков от клиента
proxy_pass http://yalarba:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
# Увеличенные таймауты для длительных операций (10 минут)
proxy_connect_timeout 600;
proxy_send_timeout 600;
proxy_read_timeout 600;
}
location /auth/ {
# Проксирование всех запросов к API на Golang сервис
proxy_pass http://api_yal/; # Контейнер Docker
# Передача оригинальных заголовков от клиента
location /api/v1/ {
proxy_pass http://api_yal:8787;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
# Увеличенные таймауты для длительных операций (10 минут)
proxy_connect_timeout 600;
proxy_send_timeout 600;
proxy_read_timeout 600;
if ($request_method = OPTIONS) {
add_header 'Access-Control-Allow-Origin' "$http_origin";
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE';
add_header 'Access-Control-Allow-Headers' 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Length' 0;
add_header 'Content-Type' 'text/plain charset=UTF-8';
return 204;
}
}
}
# ================================================
# БЛОК 3: HTTPS СЕРВЕР ДЛЯ VALITOVGAZIZ.RU
# ================================================
server {
listen 443 ssl;
server_name valitovgaziz.ru www.valitovgaziz.ru;
# Свой SSL сертификат для этого домена
ssl_certificate /etc/letsencrypt/live/valitovgaziz.ru/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/valitovgaziz.ru/privkey.pem;
# Те же настройки безопасности SSL
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
# ============================================
# ЛОКАЦИЯ: Статический сайт
# ============================================
location / {
# Статические HTML файлы
root /usr/share/nginx/valitovgaziz/html;
index index.html;
# Стандартная логика для статических сайтов
try_files $uri $uri/ /index.html;
}
# ============================================
# ЛОКАЦИЯ: API для аналитики
# ============================================
location /api/ {
# Проксирование на Node.js сервис аналитики
proxy_pass http://analytics:3000/;
# Базовые заголовки прокси
proxy_pass http://valitovgaziz/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
proxy_connect_timeout 600;
proxy_send_timeout 600;
proxy_read_timeout 600;
}
# ========================================
# НАСТРОЙКИ CORS (Cross-Origin Resource Sharing)
# ========================================
# Разрешаем запросы с ЛЮБОГО домена (*)
# Внимание: "*" может быть небезопасно в production
add_header Access-Control-Allow-Origin "*" always;
add_header Access-Control-Allow-Methods "POST, GET, OPTIONS" always;
add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
add_header Access-Control-Allow-Credentials "true" always;
location /api/ {
proxy_pass http://analytics:3000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
proxy_connect_timeout 600;
proxy_send_timeout 600;
proxy_read_timeout 600;
# Обработка предварительных OPTIONS запросов (preflight)
# Браузеры отправляют такие запросы перед основными
if ($request_method = OPTIONS) {
# 204 - No Content (успешный пустой ответ)
add_header 'Access-Control-Allow-Origin' "$http_origin";
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE';
add_header 'Access-Control-Allow-Headers' 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Length' 0;
add_header 'Content-Type' 'text/plain charset=UTF-8';
return 204;
}
# Стандартные таймауты для API аналитики
proxy_connect_timeout 30s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
}
}
# ================================================
# БЛОК 4: HTTPS СЕРВЕР ДЛЯ EASYSITE102.RU
# ================================================
server {
listen 443 ssl;
server_name easysite102.ru www.easysite102.ru;
# Свой SSL сертификат
ssl_certificate /etc/letsencrypt/live/easysite102.ru/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/easysite102.ru/privkey.pem;
# Безопасные настройки SSL
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
# ============================================
# ЛОКАЦИЯ: Проксирование к Nuxt.js приложению
# ============================================
location / {
# ВСЕ запросы проксируются к Nuxt.js серверу
proxy_pass http://easysite:3000;
# Полный набор заголовков для корректной работы приложения
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
# Длинные таймауты для работы приложения
proxy_connect_timeout 600;
proxy_send_timeout 600;
proxy_read_timeout 600;
}
# ============================================
# ЛОКАЦИЯ: API Backend для Easysite
# ============================================
location /api/ {
# Отдельный API endpoint для backend
proxy_pass http://api_es:8088/;
# Заголовки прокси
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
# Таймауты как у основного приложения
proxy_connect_timeout 600;
proxy_send_timeout 600;
proxy_read_timeout 600;
# ========================================
# ДЕТАЛЬНЫЕ НАСТРОЙКИ CORS ДЛЯ OPTIONS
# ========================================
if ($request_method = OPTIONS ) {
# Динамический заголовок Origin из запроса
add_header 'Access-Control-Allow-Origin' "$http_origin";
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE';
# Подробный список разрешенных заголовков
add_header 'Access-Control-Allow-Headers' 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
# Время кэширования preflight ответа (20 дней)
add_header 'Access-Control-Max-Age' 1728000;
# Пустой ответ для OPTIONS
add_header 'Content-Length' 0;
add_header 'Content-Type' 'text/plain charset=UTF-8';
# Возвращаем 204 без тела ответа
return 204;
}
}
}
# ================================================
# БЛОК 5: HTTPS СЕРВЕР ДЛЯ IDN ДОМЕНА
# (Punycode для "бегущийбашкир.рф")
# ================================================
server {
listen 443 ssl;
# Punycode представление кириллического домена
server_name xn--80abahjtcfl5d0a8di.xn--p1ai
www.xn--80abahjtcfl5d0a8di.xn--p1ai;
# Отдельный сертификат для IDN домена
ssl_certificate /etc/letsencrypt/live/xn--80abahjtcfl5d0a8di.xn--p1ai/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/xn--80abahjtcfl5d0a8di.xn--p1ai/privkey.pem;
# Стандартные SSL настройки
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
# ============================================
# ЛОКАЦИЯ: SPA приложение (такое же как begushiybashkir.ru)
# ============================================
location / {
root /usr/share/nginx/begushiybashkir/html;
index index.html;
try_files $uri $uri/ /index.html;
}
# ============================================
# ЛОКАЦИЯ: API для "Бегущий Башкир"
# ============================================
location /api/ {
proxy_pass http://api_bb:8080/;
location /api/v1/ {
proxy_pass http://api_yal:8787;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
@@ -345,8 +150,7 @@ server {
proxy_send_timeout 600;
proxy_read_timeout 600;
# Те же CORS настройки что и у Easysite
if ($request_method = OPTIONS ) {
if ($request_method = OPTIONS) {
add_header 'Access-Control-Allow-Origin' "$http_origin";
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE';
add_header 'Access-Control-Allow-Headers' 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
@@ -355,55 +159,26 @@ server {
add_header 'Content-Type' 'text/plain charset=UTF-8';
return 204;
}
}
# ============================================
# ЛОКАЦИЯ: Загруженные файлы (статическое обслуживание)
# ============================================
location /uploads/ {
# Обслуживание файлов загрузок напрямую из файловой системы
alias /uploads/;
# Долгое кэширование - файлы загрузок редко меняются
expires 1y;
add_header Cache-Control "public, immutable";
# try_files не нужен - nginx сам проверит существование файла
}
}
# ================================================
# БЛОК 6: HTTPS СЕРВЕР ДЛЯ BEGUSHIYBASHKIR.RU
# (ДУБЛИРУЕТ БЛОК 5 С ДРУГИМ ДОМЕНОМ)
# ================================================
server {
listen 443 ssl;
server_name begushiybashkir.ru www.begushiybashkir.ru;
# Свой SSL сертификат для этого домена
ssl_certificate /etc/letsencrypt/live/begushiybashkir.ru/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/begushiybashkir.ru/privkey.pem;
# Стандартные SSL настройки
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
# ВНИМАНИЕ: Весь контент ниже ДОСЛОВНО ДУБЛИРУЕТ
# предыдущий серверный блок для IDN домена
# ============================================
# ЛОКАЦИЯ: SPA приложение
# ============================================
location / {
root /usr/share/nginx/begushiybashkir/html;
index index.html;
try_files $uri $uri/ /index.html;
}
# ============================================
# ЛОКАЦИЯ: API для "Бегущий Башкир"
# ============================================
location /api/ {
proxy_pass http://api_bb:8080/;
proxy_set_header Host $host;
@@ -415,8 +190,7 @@ server {
proxy_send_timeout 600;
proxy_read_timeout 600;
# Копия CORS настроек
if ($request_method = OPTIONS ) {
if ($request_method = OPTIONS) {
add_header 'Access-Control-Allow-Origin' "$http_origin";
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE';
add_header 'Access-Control-Allow-Headers' 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
@@ -425,17 +199,58 @@ server {
add_header 'Content-Type' 'text/plain charset=UTF-8';
return 204;
}
}
location /uploads/ {
alias /uploads/;
expires 1y;
add_header Cache-Control "public, immutable";
}
}
server {
listen 443 ssl;
server_name xn--80abahjtcfl5d0a8di.xn--p1ai www.xn--80abahjtcfl5d0a8di.xn--p1ai;
ssl_certificate /etc/letsencrypt/live/xn--80abahjtcfl5d0a8di.xn--p1ai/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/xn--80abahjtcfl5d0a8di.xn--p1ai/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
location / {
root /usr/share/nginx/begushiybashkir/html;
index index.html;
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://api_bb:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
proxy_connect_timeout 600;
proxy_send_timeout 600;
proxy_read_timeout 600;
if ($request_method = OPTIONS) {
add_header 'Access-Control-Allow-Origin' "$http_origin";
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE';
add_header 'Access-Control-Allow-Headers' 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Length' 0;
add_header 'Content-Type' 'text/plain charset=UTF-8';
return 204;
}
}
# ============================================
# ЛОКАЦИЯ: Загруженные файлы
# ============================================
location /uploads/ {
alias /uploads/;
expires 1y;
add_header Cache-Control "public, immutable";
}
}
# ================================================
# КОНЕЦ КОНФИГУРАЦИИ
# ================================================
+49
View File
@@ -0,0 +1,49 @@
# Единый источник истины для всех сайтов проекта
# Добавление нового сайта = одна секция в этом файле
# После изменений запусти: bash generate-configs.sh
sites:
yalarba:
domain: yalarba.ru
aliases:
- www.yalarba.ru
type: upstream
upstream: http://yalarba:3000
api:
/api/v1/: http://api_yal:8787
valitovgaziz:
domain: valitovgaziz.ru
aliases:
- www.valitovgaziz.ru
type: upstream
upstream: http://valitovgaziz/
api:
/api/: http://analytics:3000/
easysite102:
domain: easysite102.ru
aliases:
- www.easysite102.ru
type: upstream
upstream: http://easysite:3000
api:
/api/v1/: http://api_yal:8787
begushiybashkir:
domain: begushiybashkir.ru
aliases:
- www.begushiybashkir.ru
type: static
root: /usr/share/nginx/begushiybashkir/html
api:
/api/: http://api_bb:8080/
begushiybashkir_idn:
domain: xn--80abahjtcfl5d0a8di.xn--p1ai
aliases:
- www.xn--80abahjtcfl5d0a8di.xn--p1ai
type: static
root: /usr/share/nginx/begushiybashkir/html
api:
/api/: http://api_bb:8080/
+8
View File
@@ -0,0 +1,8 @@
node_modules
analytics
src
package.json
package-lock.json
jsconfig.json
vite.config.js
index.html
+4
View File
@@ -0,0 +1,4 @@
FROM nginx:alpine
COPY dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
-62
View File
@@ -1,62 +0,0 @@
# ValitovGaziz - Персональный сайт и портфолио
![Version](https://img.shields.io/badge/version-1.0.0-blue.svg)
![Node.js](https://img.shields.io/badge/Node.js-18%2B-green.svg)
![License](https://img.shields.io/badge/license-MIT-lightgrey.svg)
<div align="center">
🌐 **Live Demo**: [valitovgaziz.ru](https://valitovgaziz.ru) |
💼 **Портфолио** |
🚀 **Проекты** |
👥 **Команда мечты**
</div>
## 📋 О проекте
Персональный сайт-портфолио Гализа Валитова - технологического предпринимателя и Fullstack-разработчика. Сайт представляет профессиональный профиль, проекты и возможности для сотрудничества.
### 🎯 Основные разделы:
- **Обо мне** - профессиональный профиль и подход к работе
- **Проекты** - текущие и завершенные разработки
- **Команда мечты** - приглашение к сотрудничеству
- **Yalarba.ru** - флагманский Travel Tech проект
- **Навыки** - технический стек и экспертиза
- **Опыт работы** - карьерный путь
## 🛠 Технологический стек
### Frontend
- **HTML5** - семантическая разметка
- **CSS3** - кастомные стили и анимации
- **JavaScript (ES6+)** - интерактивность и логика
- **Vue3.js** - современный фронтенд фреймворк
- **Nuxt.js 4** - SSR/SSG приложения
### Backend (Analytics Server)
- **Node.js** - серверная платформа
- **Express.js** - веб-фреймворк
- **Helmet** - безопасность HTTP заголовков
- **CORS** - кросс-доменные запросы
- **Compression** - сжатие ответов
- **Morgan** - логирование запросов
### Базы данных и инфраструктура
- **PostgreSQL** - реляционная БД
- **Docker** - контейнеризация
- **Docker Swarm** - оркестрация
## 🚀 Быстрый старт
### Предварительные требования
- Node.js 18+
- npm или yarn
- Современный браузер
### Установка и запуск
1. **Клонирование репозитория**
```bash
git clone https://github.com/valitovgaziz/valitovgaziz.ru.git
cd valitovgaziz.ru
-274
View File
@@ -1,274 +0,0 @@
# Документация: ValitovGaziz.ru
## Обзор проекта
**ValitovGaziz.ru** — это персональный сайт-портфолио Гализа Валитова, технологического предпринимателя и Fullstack-разработчика. Сайт представляет собой профессиональную визитную карточку, демонстрирующую навыки, проекты и возможности для сотрудничества.
### Основные характеристики
- **Современный дизайн** с адаптивной версткой
- **Темная/светлая тема** с автоматическим определением системных предпочтений
- **Интерактивные элементы** для вовлечения пользователей
- **Цифровой фон** с анимациями в стиле "матрицы"
- **Полностью статический** (без серверного рендеринга)
- **Оптимизирован для SEO** и доступности
---
## Структура файлов
```
valitovgaziz.ru/
├── index.html # Главная страница
├── style.css # Основной файл стилей
├── scripts.js # Основные скрипты
├── darkThemeToggle.js # Переключение темной темы
├── digital_background.js # Создание цифрового фона
├── analytics.js # Пользовательская аналитика
├── README.md # Документация проекта
├── images/ # Изображения и иконки
│ ├── ValitovGaziz/ # Фотографии
│ └── favicon/ # Иконки и логотипы
└── style/ # Стилевые файлы
├── about.css
├── darkTheme.css
├── digital_background.css
├── footer.css
├── hero_section.css
├── links_style.css
├── repository_section.css
├── saveContactsButtonStyle.css
├── skill_section.css
├── social_link.css
└── yalarba_investmen.css
```
---
## Структура сайта
### 1. Hero Section (Заголовок)
**Файлы:** `hero_section.css`, `digital_background.css`
- Главный заголовок с приветствием
- Кнопки действий "Обсудить сотрудничество" и "Написать мне"
- Социальные ссылки (Telegram, VK)
- Кнопка переключения темы
- Анимированный цифровой фон
### 2. Обо мне (About Section)
**Файлы:** `about.css`
- Фотография профиля
- Описание профессионального подхода
- Ключевые компетенции:
- Техническое видение
- Бизнес-ориентация
- Практический подход
- Мотивация
### 3. О репозитории (Repository Section)
**Файлы:** `repository_section.css`
- Сетка проектов (3 карточки):
1. ValitovGaziz.ru
2. Yalarba.ru
3. BegushiyBashkir.ru
- Информация о текущей работе
- Ссылки на GitHub и проекты
### 4. Команда мечты (Team Section)
- Приглашение к сотрудничеству
- Роли для найма:
- Программисты
- Дизайнеры
- Аналитики
- Продавцы-стратеги
- Преимущества участия
- Кнопка "Присоединиться к команде"
### 5. Yalarba.ru (Travel Tech Project)
**Файлы:** `yalarba_investmen.css`
- Описание флагманского проекта
- Технологический стек
- Статистика и ценностное предложение
- Инвестиционные возможности
### 6. Навыки (Skills Section)
**Файлы:** `skill_section.css`
- Карточки навыков с уровнями:
- Golang (Продвинутый)
- JavaScript (Продвинутый)
- Vue3 (Средний)
- Nuxt (Средний)
- PostgreSQL (Средний)
- Docker (Средний)
- Java (Начинающий)
- Spring Framework (Начинающий)
### 7. Опыт работы и образование
- Таймлайн профессионального опыта
- Образование и курсы
- Языки
### 8. Контакты
**Файлы:** `saveContactsButtonStyle.css`
- Контактная информация
- Кнопка "Сохранить контакт" (vCard формат)
- Социальные сети и мессенджеры
### 9. Футер
**Файлы:** `footer.css`
- Технологии
- Контакты
- Сообщество
- Авторские права
---
## Технические особенности
### Темная тема
**Файлы:** `darkTheme.css`, `darkThemeToggle.js`
- Автоматическое определение системных предпочтений
- Сохранение выбора в localStorage
- Полная поддержка всех элементов интерфейса
### Цифровой фон
**Файлы:** `digital_background.css`, `digital_background.js`
- Анимированные двоичные потоки (бинарный дождь)
- Плавающие элементы кода
- Точки соединений и линии передачи данных
- Адаптация под текущую тему
### Аналитика
**Файл:** `analytics.js`
- Пользовательская система сбора данных
- Отслеживание событий и кликов
- Очередь с автосохранением в localStorage
- Отправка данных на сервер при возможности
- Отслеживание видимости секций
### Ссылки
**Файл:** `links_style.css`
- Анимированные внешние ссылки с иконками
- Индикация внутренних/внешних ссылок
- Адаптация под тему
---
## Технологический стек
### Frontend
- **HTML5** — семантическая разметка
- **CSS3** — Grid, Flexbox, CSS Variables, анимации
- **JavaScript (ES6+)** — нативный JS без фреймворков
- **CSS Grid Layout** — основная система верстки
### Особенности CSS
- CSS Custom Properties (переменные) для тем
- CSS Grid для сложных макетов
- CSS Flexbox для простых выравниваний
- CSS Animations для интерактивности
- Media Queries для адаптивности
### JavaScript функциональность
- Динамическое переключение тем
- Создание интерактивного фона
- Обработка форм и кнопок
- Сохранение контактов в vCard формате
- Интеграция с Telegram API
---
## SEO и доступность
### Мета-теги
- Полный набор meta-тегов для SEO
- Ключевые слова для IT-специалистов и предпринимателей
- Атрибуты для доступности (alt, aria)
### Оптимизация
- Ленивая загрузка изображений
- Минификация CSS и JS
- Оптимизированные шрифты
- Быстрая загрузка страницы
### Адаптивность
- Mobile-first подход
- 4 точки останова:
- < 480px (мобильные)
- 480px - 768px (планшеты)
- 769px - 1024px (ноутбуки)
- > 1024px (десктопы)
---
## Интеграции
### Telegram
- Отправка сообщений через Telegram Bot API
- Обработка кнопок "Написать мне"
- Форма для отправки предложений
### vCard
- Генерация контактов в формате vCard
- Автоматическое скачивание контакта
---
## Рекомендации по развитию
### Для добавления нового раздела:
1. Создайте HTML структуру в `index.html`
2. Добавьте стили в соответствующий CSS файл
3. Подключите через `@import` в `style.css`
4. Добавьте поддержку темной темы
5. Интегрируйте с аналитикой
### Для модификации существующего:
1. Найдите соответствующий CSS файл
2. Внесите изменения с учетом адаптивности
3. Проверьте поддержку темной темы
4. Протестируйте на разных устройствах
---
## Производительность
### Рекомендации по оптимизации:
1. **Изображения:** Используйте WebP формат с JPEG/PNG fallback
2. **Шрифты:** Локальное хранение системных шрифтов
3. **JavaScript:** Дефер загрузки скриптов
4. **CSS:** Критический CSS в head
### Мониторинг:
- Встроенная аналитика отслеживает загрузку страниц
- Google Analytics можно подключить через `analytics.js`
- Рекомендуется использовать Lighthouse для аудита
---
## Поддержка браузеров
- **Chrome** 60+
- **Firefox** 55+
- **Safari** 12+
- **Edge** 79+
- **iOS Safari** 12+
- **Android Chrome** 60+
---
## Лицензия
Проект распространяется под лицензией MIT. Все изображения и контент защищены авторскими правами Гализа Валитова.
---
## Контакты для поддержки
- **Телеграм:** [@valitovgaziz](https://t.me/valitovgaziz)
- **Email:** valitovgaziz@yandex.ru
- **GitHub:** [valitovgaziz](https://github.com/valitovgaziz)
- **Сайт:** [valitovgaziz.ru](https://valitovgaziz.ru)
---
*Последнее обновление документации: 2025*
@@ -1,270 +0,0 @@
// analytics.js - собственный счетчик аналитики для браузера
class CustomAnalytics {
constructor() {
this.endpoint = 'https://valitovgaziz.ru/api/analytics'; // Ваш endpoint для сбора данных
this.queue = [];
this.isOnline = navigator.onLine;
this.sessionId = this.getSessionId();
this.init();
}
init() {
// Загружаем сохраненные данные из localStorage
this.loadFromStorage();
// Отслеживание событий
this.trackPageView();
this.setupEventListeners();
// Периодическая отправка данных
setInterval(() => this.flushQueue(), 30000);
// Отслеживание онлайн/офлайн статуса
window.addEventListener('online', () => {
this.isOnline = true;
this.flushQueue();
});
window.addEventListener('offline', () => {
this.isOnline = false;
});
// Отправка данных перед закрытием страницы
window.addEventListener('beforeunload', () => {
this.trackEvent('page', 'unload');
this.flushQueueSync();
});
}
trackPageView() {
const data = {
type: 'pageview',
url: window.location.href,
referrer: document.referrer,
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent,
screen: `${screen.width}x${screen.height}`,
language: navigator.language,
sessionId: this.sessionId
};
this.addToQueue(data);
}
trackEvent(category, action, label = null, value = null) {
const data = {
type: 'event',
category,
action,
label,
value,
timestamp: new Date().toISOString(),
url: window.location.href,
sessionId: this.sessionId
};
this.addToQueue(data);
}
trackClick(element, context = 'unknown') {
const data = {
type: 'click',
element: element.tagName,
text: element.textContent?.substring(0, 100),
context,
timestamp: new Date().toISOString(),
url: window.location.href,
sessionId: this.sessionId
};
this.addToQueue(data);
}
addToQueue(data) {
this.queue.push(data);
// Сохраняем в localStorage
this.saveToStorage();
// Отправляем сразу если онлайн и очередь большая
if (this.isOnline && this.queue.length >= 3) {
this.flushQueue();
}
}
async flushQueue() {
if (!this.isOnline || this.queue.length === 0) return;
const batch = [...this.queue];
try {
const response = await fetch(this.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
events: batch,
sessionId: this.sessionId
}),
keepalive: true // Позволяет отправлять данные даже при закрытии страницы
});
if (response.ok) {
// Удаляем отправленные данные из очереди
this.queue = this.queue.filter(item => !batch.includes(item));
this.saveToStorage();
}
} catch (error) {
console.log('Analytics offline, storing locally');
}
}
flushQueueSync() {
if (this.queue.length === 0) return;
// Используем sendBeacon для надежной отправки при закрытии страницы
const data = JSON.stringify({
events: this.queue,
sessionId: this.sessionId
});
if (navigator.sendBeacon) {
navigator.sendBeacon(this.endpoint, data);
}
}
getSessionId() {
let sessionId = localStorage.getItem('ga_session_id');
const now = Date.now();
if (!sessionId) {
sessionId = 'sess_' + now + '_' + Math.random().toString(36).substr(2, 9);
localStorage.setItem('ga_session_id', sessionId);
localStorage.setItem('ga_session_start', now);
}
// Обновляем время последней активности
localStorage.setItem('ga_last_activity', now);
return sessionId;
}
saveToStorage() {
try {
localStorage.setItem('ga_queue', JSON.stringify(this.queue));
} catch (e) {
console.warn('Cannot save analytics to localStorage');
}
}
loadFromStorage() {
try {
const stored = localStorage.getItem('ga_queue');
if (stored) {
const parsed = JSON.parse(stored);
if (Array.isArray(parsed)) {
this.queue = parsed;
}
}
} catch (e) {
console.warn('Cannot load analytics from localStorage');
}
}
setupEventListeners() {
// Отслеживание кликов по кнопкам
document.addEventListener('click', (e) => {
if (e.target.matches('button, .btn, a[href]')) {
const context = e.target.closest('.section') ?
e.target.closest('.section').querySelector('h2')?.textContent || 'unknown' :
'global';
this.trackClick(e.target, context);
// Специальные события для кнопок сотрудничества
if (e.target.textContent.includes('сотрудничество') || e.target.textContent.includes('Написать')) {
this.trackEvent('conversion', 'contact_click', e.target.textContent.trim());
}
}
});
// Отслеживание отправки форм
document.addEventListener('submit', (e) => {
this.trackEvent('form', 'submit', e.target.id || 'unknown');
});
// Отслеживание видимости секций
this.setupSectionTracking();
// Отслеживание внешних ссылок
document.addEventListener('click', (e) => {
const link = e.target.closest('a[href]');
if (link && link.hostname !== window.location.hostname) {
this.trackEvent('outbound', 'click', link.href);
}
});
}
setupSectionTracking() {
const sections = document.querySelectorAll('.section');
const observedSections = new Set();
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting && entry.intersectionRatio >= 0.5) {
const sectionId = entry.target.id ||
entry.target.querySelector('h2')?.textContent?.substring(0, 50) ||
'unknown_section';
if (!observedSections.has(sectionId)) {
observedSections.add(sectionId);
this.trackEvent('content', 'section_view', sectionId);
}
}
});
}, {
threshold: [0.5],
rootMargin: '0px 0px -10% 0px'
});
sections.forEach(section => {
observer.observe(section);
});
}
}
// Инициализация при полной загрузке DOM
document.addEventListener('DOMContentLoaded', () => {
window.analytics = new CustomAnalytics();
// Глобальные функции для ручного отслеживания
window.trackEvent = (category, action, label, value) => {
if (window.analytics) {
window.analytics.trackEvent(category, action, label, value);
}
};
// Отслеживание специальных событий для вашего сайта
const specialButtons = document.querySelectorAll('[onclick*="sendMessageTelegram"]');
specialButtons.forEach(btn => {
btn.addEventListener('click', () => {
trackEvent('business', 'telegram_click', btn.textContent.trim());
});
});
// Отслеживание просмотра ключевых элементов
const keyElements = document.querySelectorAll('.hero, .team-section, .yalarba-section');
const elementObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const elementType = entry.target.className.split(' ')[0];
trackEvent('engagement', `${elementType}_viewed`);
elementObserver.unobserve(entry.target);
}
});
}, { threshold: 0.3 });
keyElements.forEach(el => elementObserver.observe(el));
});
// Fallback для старых браузеров
if (!window.Promise) {
console.warn('Custom analytics requires Promise support');
window.trackEvent = function () { };
}
Binary file not shown.
-590
View File
@@ -1,590 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description"
content="Блог Валитова Газиза - мысли, проекты, обновления и размышления о разработке и предпринимательстве">
<title>Блог | ValitovGaziz - Мысли и обновления</title>
<link rel="icon" href="./images/favicon/code_orange.png">
<link rel="stylesheet" href="style/blog.css" />
</head>
<body>
<!-- Кнопка переключения темы -->
<button class="theme-toggle" onclick="toggleTheme()">
🌙 Темная тема
</button>
<!-- Навигация -->
<nav class="blog-nav">
<div class="blog-nav-container">
<a href="index.html" class="blog-nav-logo">ValitovGaziz</a>
<a href="index.html" class="blog-nav-link">← На главную</a>
</div>
</nav>
<!-- Заголовок блога -->
<header class="blog-header">
<div class="blog-header-content">
<h1 class="blog-title">Блог</h1>
<p class="blog-subtitle">Мысли, проекты и обновления из мира разработки и предпринимательства</p>
<div class="blog-meta">
<span class="blog-meta-item">📝 Личный блог</span>
<span class="blog-meta-item">🔄 Регулярные обновления</span>
<span class="blog-meta-item">🎯 Фокус на содержании</span>
</div>
</div>
</header>
<main class="blog-container">
<!-- Кнопка для мобильного меню (скрыта на десктопе) -->
<button class="blog-sidebar-toggle" onclick="toggleSidebar()">
📂 Меню блога
</button>
<!-- Основное содержание блога - ЛЕВАЯ КОЛОНКА (70%) -->
<div class="blog-content">
<!-- Пример записи блога -->
<article class="blog-post" id="post1">
<header class="blog-post-header">
<span class="blog-post-category">Проекты</span>
<h2 class="blog-post-title">Новый этап развития Yalarba.ru</h2>
<div class="blog-post-meta">
<time datetime="2024-03-15">15 марта 2024</time>
<span></span>
<span>5 минут чтения</span>
</div>
</header>
<div class="blog-post-content">
<p>Сегодня хочу поделиться важным обновлением по проекту Yalarba.ru. Мы завершили переход на новую
архитектуру и готовимся к запуску нескольких ключевых функций, которые существенно улучшат
пользовательский опыт.</p>
<h3>Что изменилось:</h3>
<ul>
<li>Полностью переработанный интерфейс поиска маршрутов</li>
<li>Интеграция с картографическими сервисами</li>
<li>Улучшенная система рекомендаций</li>
<li>Подготовка к мобильному приложению</li>
</ul>
<p>Этот этап занял больше времени, чем планировалось, но результат того стоит. Особенно горжусь тем,
как команда справилась с техническими вызовами.</p>
<blockquote class="blog-quote">
"Технологии должны решать реальные проблемы людей, а не создавать новые"
</blockquote>
<p>В ближайших планах — запуск бета-тестирования новых функций и привлечение первых партнеров из
туристической отрасли.</p>
</div>
<footer class="blog-post-footer">
<div class="blog-post-tags">
<a href="#" class="blog-tag">#Yalarba</a>
<a href="#" class="blog-tag">#TravelTech</a>
<a href="#" class="blog-tag">#Разработка</a>
</div>
<button onclick="sendMessageTelegram()" class="blog-comment-btn">
💬 Обсудить
</button>
</footer>
</article>
<!-- Вторая запись -->
<article class="blog-post" id="post2">
<header class="blog-post-header">
<span class="blog-post-category">Разработка</span>
<h2 class="blog-post-title">Переход с Vue 2 на Vue 3: опыт и выводы</h2>
<div class="blog-post-meta">
<time datetime="2024-03-10">10 марта 2024</time>
<span></span>
<span>7 минут чтения</span>
</div>
</header>
<div class="blog-post-content">
<p>После нескольких месяцев работы с Vue 3 в продакшене хочу поделиться наблюдениями о переходе с
Vue 2.</p>
<h3>Основные преимущества:</h3>
<ol>
<li><strong>Composition API</strong> — действительно улучшает переиспользование кода</li>
<li><strong>Улучшенная производительность</strong> — заметный прирост в больших приложениях</li>
<li><strong>TypeScript поддержка</strong> — наконец-то полноценная интеграция</li>
<li><strong>Меньший размер бандла</strong> — tree-shaking работает лучше</li>
</ol>
<h3>Сложности перехода:</h3>
<p>Не всё прошло гладко. Некоторые библиотеки ещё не обновились, пришлось искать альтернативы или
писать собственные решения. Также Composition API требует изменения мышления, особенно для
разработчиков, долго работавших с Options API.</p>
<p>В целом, переход оправдан. Особенно для новых проектов — рекомендую сразу начинать с Vue 3.</p>
</div>
<footer class="blog-post-footer">
<div class="blog-post-tags">
<a href="#" class="blog-tag">#Vue3</a>
<a href="#" class="blog-tag">#Frontend</a>
<a href="#" class="blog-tag">#JavaScript</a>
</div>
<button onclick="sendMessageTelegram()" class="blog-comment-btn">
💬 Обсудить
</button>
</footer>
</article>
<!-- Третья запись -->
<article class="blog-post" id="post3">
<header class="blog-post-header">
<span class="blog-post-category">Мысли</span>
<h2 class="blog-post-title">О важности сообщества в разработке</h2>
<div class="blog-post-meta">
<time datetime="2024-03-05">5 марта 2024</time>
<span></span>
<span>4 минуты чтения</span>
</div>
</header>
<div class="blog-post-content">
<p>В последнее время всё чаще задумываюсь о том, насколько важно окружение для профессионального
роста. Особенно в IT, где технологии меняются так быстро.</p>
<p>Когда работаешь один, легко застрять в своих паттернах, не замечать новые подходы или повторять
одни и те же ошибки. Сообщество — это не только нетворкинг, это:</p>
<ul>
<li><strong>Обратная связь</strong> — свежий взгляд на твои решения</li>
<li><strong>Совместное обучение</strong> — каждый знает что-то, чего не знаешь ты</li>
<li><strong>Поддержка</strong> — особенно важна в сложные периоды</li>
<li><strong>Вдохновение</strong> — видеть успехи других мотивирует</li>
</ul>
<p>Именно поэтому я решил больше инвестировать в развитие сообщества вокруг своих проектов. Если вы
читаете это — возможно, нам стоит пообщаться :)</p>
</div>
<footer class="blog-post-footer">
<div class="blog-post-tags">
<a href="#" class="blog-tag">#Сообщество</a>
<a href="#" class="blog-tag">#Развитие</a>
<a href="#" class="blog-tag">#IT</a>
</div>
<button onclick="sendMessageTelegram()" class="blog-comment-btn">
💬 Присоединиться
</button>
</footer>
</article>
<!-- Четвёртая запись -->
<article class="blog-post" id="post4">
<header class="blog-post-header">
<span class="blog-post-category">Проекты</span>
<h2 class="blog-post-title">EasySite & YalArba: Текущее состояние и роадмап развития</h2>
<div class="blog-post-meta">
<time datetime="2024-03-20">20 марта 2024</time>
<span></span>
<span>6 минут чтения</span>
</div>
</header>
<div class="blog-post-content">
<p>С момента запуска первых версий <strong>EasySite102.ru</strong> и <strong>YalArba.ru</strong>
прошло
несколько месяцев интенсивной разработки. Хочу поделиться текущим состоянием проекта,
достигнутыми
результатами и планами на ближайшее будущее.</p>
<h3>🎯 Суть проекта сегодня</h3>
<p>Мы строим полноценную экосистему для туристического рынка:</p>
<ul>
<li><strong>EasySite (B2B)</strong> — конструктор сайтов для владельцев отелей, санаториев,
ресторанов и
достопримечательностей</li>
<li><strong>YalArba (B2C)</strong> — агрегатор для туристов с поиском, отзывами, маршрутами и
системой
бронирования</li>
</ul>
<h3>✅ Что уже работает (стабильно в продакшене)</h3>
<ul>
<li><strong>JWT-аутентификация</strong> — безопасный вход для всех типов пользователей</li>
<li><strong>Полностью контейнеризованная инфраструктура</strong> — Docker, Docker Compose</li>
<li><strong>SSL шифрование</strong> — HTTPS на всех доменах через Let's Encrypt</li>
<li><strong>Базовая аналитика</strong> — отслеживание посещений и пользовательского поведения
</li>
</ul>
<h3>🛠️ Технологический стек (актуальный)</h3>
<div class="tech-stack">
<div class="tech-item">
<strong>Frontend:</strong> Nuxt.js 3 (EasySite), Vue 3 + Composition API (YalArba)
</div>
<div class="tech-item">
<strong>Backend:</strong> Go (Golang) с использованием GORM, Chi
</div>
<div class="tech-item">
<strong>База данных:</strong> PostgreSQL (раздельные инстансы для разных сервисов)
</div>
<div class="tech-item">
<strong>Инфраструктура:</strong> Docker, Nginx, система автоматического обновления SSL
</div>
</div>
<h3>📊 API-архитектура</h3>
<p>Проект построен по микросервисной архитектуре:</p>
<ul>
<li><strong>EasySite API:</strong> <code>localhost:8088/docs</code> (управление сайтами)</li>
<li><strong>YalArba API:</strong> <code>localhost:8888/docs</code> (поиск и бронирование)</li>
<li><strong>Auth Service:</strong> централизованная аутентификация</li>
</ul>
<blockquote class="blog-quote">
"Статус проекта на 20.03.2026: 🟢 Активная разработка. Основная функциональность работает, идёт
наполнение
контентом и привлечение первых пользователей."
</blockquote>
<h3>📅 Роадмап развития (2026 год)</h3>
<p>Приоритеты на ближайшие месяцы:</p>
<h4>Q3 2026 (Июль–Сентябрь)</h4>
<ul>
<li><strong>Платежная система</strong> — интеграция с ЮKassa, Tinkoff</li>
<li><strong>Мультиязычность</strong> — поддержка английского и башкирского языков</li>
<li><strong>API для партнеров</strong> — возможность интеграции сторонних сервисов</li>
<li><strong>Система кэширования</strong> — Redis для повышения производительности</li>
</ul>
<h4>Q4 2026 (Октябрь–Декабрь)</h4>
<ul>
<li><strong>Мобильные приложения</strong> — iOS и Android (React Native)</li>
<li><strong>Система рекомендаций</strong> — AI-based подборки на основе поведения</li>
<li><strong>Масштабирование инфраструктуры</strong> — переход на Kubernetes</li>
<li><strong>Реферальная программа</strong> — для владельцев и туристов</li>
</ul>
<h3>👥 Командная ситуация</h3>
<p>Сейчас проект развивается силами небольшой команды (2 человека). Мы активно ищем:</p>
<ul>
<li><strong>Frontend-разработчиков</strong> (Vue 3, Nuxt.js)</li>
<li><strong>Дизайнеров UI/UX</strong></li>
<li><strong>Маркетологов</strong> для продвижения в туристической нише</li>
<li><strong>Контент-менеджеров</strong> для наполнения платформы</li>
</ul>
<h3>🎯 Когда ждать полноценного запуска?</h3>
<p><strong>Бета-версия с основной функциональностью</strong> уже доступна по адресам:</p>
<ul>
<li><a href="https://easysite102.ru" target="_blank">easysite102.ru</a> (для владельцев)</li>
<li><a href="https://yalarba.ru" target="_blank">yalarba.ru</a> (для туристов)</li>
</ul>
<p><strong>Полноценный запуск</strong> с платежами и мобильным приложением планируется на
<strong>сентябрь
2026</strong>.
</p>
<p><strong>Масштабирование на весь Урал и Поволжье</strong> — цель на <strong>2026 год</strong>.</p>
<h3>💬 Как можно поучаствовать?</h3>
<p>Проект открыт для сотрудничества в разных форматах:</p>
<ul>
<li><strong>Технические специалисты</strong> — присоединяйтесь к разработке (удаленно)</li>
<li><strong>Владельцы туристических объектов</strong> — создайте свой сайт на EasySite</li>
<li><strong>Инвесторы и партнеры</strong> — обсуждаем стратегическое сотрудничество</li>
<li><strong>Тестировщики</strong> — помогайте улучшать пользовательский опыт</li>
</ul>
<p>Если вас заинтересовал проект — давайте обсудим возможности сотрудничества!</p>
</div>
<footer class="blog-post-footer">
<div class="blog-post-tags">
<a href="#" class="blog-tag">#EasySite</a>
<a href="#" class="blog-tag">#YalArba</a>
<a href="#" class="blog-tag">#Туризм</a>
<a href="#" class="blog-tag">#Разработка</a>
<a href="#" class="blog-tag">#Стартап</a>
</div>
<button onclick="sendMessageTelegram()" class="blog-comment-btn">
💬 Обсудить проект
</button>
</footer>
</article>
<!-- Пятая запись (новая) -->
<article class="blog-post" id="post5">
<header class="blog-post-header">
<span class="blog-post-category">Мысли</span>
<h2 class="blog-post-title">Зачем я создаю YalArba: история и миссия</h2>
<div class="blog-post-meta">
<time datetime="2024-03-25">25 марта 2024</time>
<span></span>
<span>8 минут чтения</span>
</div>
</header>
<div class="blog-post-content">
<p>Эта история началась в 2017 году, когда я работал на заводе УМПО и параллельно учился в УКСиВТ.
Зимой
захотелось отдохнуть — съездить куда-нибудь на машине или просто развеяться в парке. Я, конечно,
полез в
интернет искать сайты и информацию. И ни на одном сайте не смог найти маршрут или место, куда
можно сходить
бесплатно.</p>
<p>Везде мне продавали туры, гостиницы, ещё много вариантов, которые для меня, простого рабочего,
совершенно не
имели никакой ценности. Пришлось искать через знакомых, через группы, куда можно поехать на
отдых с
корзинкой, бутербродами, на своей машине.</p>
<blockquote class="blog-quote">
«После этого случая мне сильно захотелось создать приложение, которое приводило бы людей к
простому и
быстрому решению по отдыху. Особенно это ценно для рабочих, у которых нет особой насмотренности,
много
возможностей и ресурсов для отдыха вдали от дома или за границей.»
</blockquote>
<h3>Социальность проекта</h3>
<p>Большая часть услуг будет бесплатной для всех, включая предпринимателей. Потому что я сам работал
на заводе и
был всегда (большую часть времени) за станком. Но остальная жизнь тогда больше походила на
несистематизированные пьянки и гулянки. В то время это было интересно, сейчас это совершенно не
вписывается
в моё мировоззрение.</p>
<p>Мне кажется, в те годы мне не хватало широты взгляда, в общем, некому было подсказать, что
отдыхать можно
по-другому. Что есть много исторических мест, памятников природы. Я просто не видел альтернативы
своему
образу отдыха.</p>
<h3>Миссия сегодня</h3>
<p>Сейчас я надеюсь, что смогу предоставить эту альтернативу. Зумеры, конечно, уже меньше подвержены
старым
способам отдыха (алкоголь употребляют меньше). Но я хочу добавить приложение (веб-портал),
которое сможет
подсказать, подкинуть идею, что отдых может быть более культурным, не таким дорогим. И главное —
недалеко от
дома. В рамках района, области или страны.</p>
<p>И я уверен, что это будет работать и в других странах. Ведь везде есть просто очень занятые люди,
всё
внимание которых направлено на работу и дом. В этом я вижу мейнстрим, большую цель для своего
приложения.
</p>
<h3>Международный потенциал</h3>
<p>Через это же приложение можно будет привлекать самостоятельных туристов в нашу страну — через
рекламу,
распространение в другие страны. Открывать наши места отдыха не только для внутреннего туриста,
но и для
иностранного (выборочно, конечно).</p>
<h3>Бизнес-модель</h3>
<p>Основная прибыль в этом проекте спрятана в количестве пользователей, которые будут пользоваться
порталом
(приложением). Конечно, приложение должно буквально делать за пользователя часть работы по
поиску, подбору,
исследованию и выбору маршрутов отдыха — чтобы получить наилучший результат.</p>
<h3>Личный путь</h3>
<p>Поставленная высокая цель помогает мне добиваться высоких результатов в жизни. Для реализации
проекта я
выучил несколько языков программирования, английский язык, добился от себя внятных установок на
жизнь,
развил в себе планирование и смог познакомиться с невероятным количеством людей. Каждый новый
рубль,
потраченный на этом пути, будет воздан.</p>
<h3>Инвестиции или самостоятельная разработка?</h3>
<p>Часто ловлю себя на мысли: а нужны ли мне инвестиции? И да, я часто и с большой уверенностью
говорю: да!
Нужны. На сервер, на человеко-часы, на заказные части программы. С другой стороны, передо мной
часто
возникает дилемма — хочется сделать всё самому.</p>
<p>Это, конечно, ошибка, которая уже стоила мне пары лет в разработке и ещё аукнется большим
количеством
времени, потраченным на разработку приложения самостоятельно. Я всё ещё на что-то надеюсь, что
как-то смогу
завершить приложение (я смогу). Просто это будет не так пафосно и круто, как хотелось бы. И
дальше, конечно,
встанет вопрос о том, как же его продавать (продвигать). Здесь уже заложены некоторые
маркетинговые фишки и
ходы для создания нового рынка и выхода на существующие.</p>
<p>В данный момент больше стараюсь уделять время семье и дому. Но часть моих усилий всегда
направлена на работу
над проектом. Конкретно сейчас работаю над блогом для проекта, хотя, казалось бы, должен
вгрызаться в
реализацию серверного приложения на Golang (gorm, chi).</p>
<p>Но я верю, что этот блог — тоже часть пути. Часть истории, которую я хочу рассказать. Чтобы
другие, кто,
возможно, оказался в похожей ситуации, знали: альтернатива есть. И мы её создаём.</p>
</div>
<footer class="blog-post-footer">
<div class="blog-post-tags">
<a href="#" class="blog-tag">#История</a>
<a href="#" class="blog-tag">#Миссия</a>
<a href="#" class="blog-tag">#СоциальныйПроект</a>
<a href="#" class="blog-tag">#Туризм</a>
<a href="#" class="blog-tag">#Развитие</a>
</div>
<button onclick="sendMessageTelegram()" class="blog-comment-btn">
💬 Обсудить идею
</button>
</footer>
</article>
</div>
<!-- Боковая панель - ПРАВАЯ КОЛОНКА (30%) -->
<aside class="blog-sidebar">
<div class="blog-sidebar-section">
<h3>О блоге</h3>
<p>Здесь я делюсь мыслями о разработке, обновлениями проектов и размышлениями о технологическом
предпринимательстве.</p>
</div>
<div class="blog-sidebar-section">
<h3>Категории</h3>
<ul class="blog-categories">
<li><a href="#" class="blog-category">Проекты</a></li>
<li><a href="#" class="blog-category">Разработка</a></li>
<li><a href="#" class="blog-category">Предпринимательство</a></li>
<li><a href="#" class="blog-category">Мысли</a></li>
<li><a href="#" class="blog-category">Обновления</a></li>
</ul>
</div>
<div class="blog-sidebar-section">
<h3>Последние записи</h3>
<ul class="blog-recent">
<li><a href="#post5">Зачем я создаю YalArba: история и миссия</a></li>
<li><a href="#post4">EasySite & YalArba: состояние и планы от Январь 2026</a></li>
<li><a href="#post1">Новые возможности Yalarba.ru</a></li>
<li><a href="#post2">Переход на Vue 3</a></li>
</ul>
</div>
</aside>
</main>
<!-- Пагинация -->
<div class="blog-pagination">
<a href="#" class="blog-pagination-btn blog-pagination-prev">← Назад</a>
<span class="blog-pagination-current">Страница 1 из 4</span>
<a href="#" class="blog-pagination-btn blog-pagination-next">Вперед →</a>
</div>
<!-- Футер блога -->
<footer class="blog-footer">
<div class="blog-footer-content">
<p>© 2024 Блог Валитова Газиза. Все записи — личные размышления и опыт.</p>
<p>
<a href="index.html">На главную</a>
<a href="https://t.me/valitovgaziz">Telegram</a>
<a href="mailto:valitovgaziz@yandex.ru">Email</a>
</p>
</div>
</footer>
<script>
// Функция для переключения темы
function toggleTheme() {
document.body.classList.toggle('dark-mode');
const btn = document.querySelector('.theme-toggle');
if (document.body.classList.contains('dark-mode')) {
btn.textContent = '☀️ Светлая тема';
localStorage.setItem('blog-theme', 'dark');
} else {
btn.textContent = '🌙 Темная тема';
localStorage.setItem('blog-theme', 'light');
}
}
// Загрузка сохраненной темы
document.addEventListener('DOMContentLoaded', function () {
const savedTheme = localStorage.getItem('blog-theme');
const btn = document.querySelector('.theme-toggle');
if (savedTheme === 'dark') {
document.body.classList.add('dark-mode');
btn.textContent = '☀️ Светлая тема';
} else {
btn.textContent = '🌙 Темная тема';
}
// Адаптация для мобильных устройств
if (window.innerWidth < 768) {
const sidebar = document.querySelector('.blog-sidebar');
const toggleBtn = document.querySelector('.blog-sidebar-toggle');
toggleBtn.style.display = 'block';
sidebar.style.display = 'none';
}
});
// Функция для отправки сообщения в Telegram
function sendMessageTelegram() {
window.open('https://t.me/valitovgaziz', '_blank');
}
// Функция для переключения сайдбара на мобильных
function toggleSidebar() {
const sidebar = document.querySelector('.blog-sidebar');
sidebar.style.display = sidebar.style.display === 'block' ? 'none' : 'block';
}
// Обработчик изменения размера окна
window.addEventListener('resize', function () {
const sidebar = document.querySelector('.blog-sidebar');
const toggleBtn = document.querySelector('.blog-sidebar-toggle');
if (window.innerWidth >= 768) {
sidebar.style.display = 'block';
toggleBtn.style.display = 'none';
} else {
toggleBtn.style.display = 'block';
sidebar.style.display = 'none';
}
});
// Плавная прокрутка для якорных ссылок
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
const targetId = this.getAttribute('href');
if (targetId === '#') return;
const targetElement = document.querySelector(targetId);
if (targetElement) {
window.scrollTo({
top: targetElement.offsetTop - 100,
behavior: 'smooth'
});
// Закрываем сайдбар на мобильных
if (window.innerWidth < 768) {
const sidebar = document.querySelector('.blog-sidebar');
sidebar.style.display = 'none';
}
}
});
});
</script>
</body>
</html>
@@ -1,25 +0,0 @@
function toggleTheme() {
document.body.classList.toggle('dark-mode');
const btn = document.querySelector('.theme-toggle');
if (document.body.classList.contains('dark-mode')) {
btn.textContent = '☀️ Светлая тема';
localStorage.setItem('theme', 'dark');
} else {
btn.textContent = '🌙 Темная тема';
localStorage.setItem('theme', 'light');
}
}
// Загрузка темы при загрузке страницы
document.addEventListener('DOMContentLoaded', function() {
const savedTheme = localStorage.getItem('theme');
const btn = document.querySelector('.theme-toggle');
if (savedTheme === 'dark') {
document.body.classList.add('dark-mode');
btn.textContent = '☀️ Светлая тема';
} else {
btn.textContent = '🌙 Темная тема';
}
});
@@ -1,174 +0,0 @@
// Digital Background Initialization
// Обновляем функцию для интеграции с темной темой
function updateBackgroundForTheme() {
const isDarkMode = document.body.classList.contains('dark-mode');
const binaryDigits = document.querySelectorAll('.binary-digit');
const floatingCode = document.querySelectorAll('.floating-code');
const connectionNodes = document.querySelectorAll('.connection-node');
const dataFlows = document.querySelectorAll('.data-flow');
// Обновляем цвета элементов в реальном времени
const accentColor = isDarkMode ? 'rgba(41, 128, 185, 0.8)' : 'rgba(0, 123, 255, 0.8)';
binaryDigits.forEach(digit => {
digit.style.color = accentColor;
});
floatingCode.forEach(code => {
code.style.color = accentColor;
});
connectionNodes.forEach(node => {
node.style.background = accentColor;
});
dataFlows.forEach(flow => {
flow.style.background = `linear-gradient(90deg, transparent, ${accentColor}, transparent)`;
});
}
// Create binary rain effect
function createBinaryRain() {
const container = document.createElement('div');
container.className = 'binary-rain';
document.body.appendChild(container);
// Создаем больше потоков для полного покрытия
for (let i = 0; i < 15; i++) { // Увеличиваем количество потоков
setTimeout(() => {
createBinaryStream(container);
}, i * 150);
}
}
function createBinaryStream(container) {
const stream = document.createElement('div');
stream.className = 'binary-stream';
// Распределяем потоки по всей ширине экрана
const left = Math.random() * 100;
stream.style.left = `${left}%`;
stream.style.position = 'absolute';
stream.style.width = 'auto';
// Создаем больше цифр в каждом потоке
for (let i = 0; i < 15; i++) {
const digit = document.createElement('div');
digit.className = 'binary-digit';
digit.textContent = Math.random() > 0.5 ? '1' : '0';
digit.style.position = 'absolute';
digit.style.left = '0';
digit.style.top = `${-i * 50}px`; // Увеличиваем расстояние между цифрами
digit.style.animationDuration = `${2 + Math.random() * 3}s`; // Быстрее анимация
digit.style.animationDelay = `${i * 0.15}s`;
digit.style.opacity = `${0.3 + Math.random() * 0.7}`; // Разная прозрачность
digit.style.fontSize = `${12 + Math.random() * 8}px`; // Разный размер шрифта
stream.appendChild(digit);
}
container.appendChild(stream);
}
// Create floating code elements
function createFloatingCode() {
const symbols = ['{', '}', '<>', '();', '[]', '</>', '=>', '&&', 'function', 'const', 'let', 'var', 'class', 'import', 'export', 'return'];
const classes = ['code-bracket', 'code-parenthesis', 'code-brace', 'code-tag'];
// Создаем больше плавающих элементов
for (let i = 0; i < 25; i++) {
const symbol = symbols[Math.floor(Math.random() * symbols.length)];
const element = document.createElement('div');
element.className = `floating-code ${classes[Math.floor(Math.random() * classes.length)]}`;
element.textContent = symbol;
element.style.left = `${Math.random() * 100}%`;
element.style.top = `${Math.random() * 100}%`;
element.style.animationDuration = `${20 + Math.random() * 20}s`;
element.style.fontSize = `${10 + Math.random() * 6}px`;
element.style.opacity = `${0.05 + Math.random() * 0.1}`;
document.body.appendChild(element);
}
}
// Create connection nodes
function createConnectionNodes() {
for (let i = 0; i < 20; i++) {
const node = document.createElement('div');
node.className = 'connection-node';
node.style.left = `${Math.random() * 100}%`;
node.style.top = `${Math.random() * 100}%`;
node.style.animationDelay = `${Math.random() * 4}s`;
node.style.width = `${4 + Math.random() * 6}px`;
node.style.height = node.style.width;
document.body.appendChild(node);
}
}
// Create data flow lines
function createDataFlows() {
for (let i = 0; i < 12; i++) {
const flow = document.createElement('div');
flow.className = 'data-flow';
flow.style.top = `${Math.random() * 100}%`;
flow.style.width = `${40 + Math.random() * 50}%`;
flow.style.left = `${-Math.random() * 30}%`;
flow.style.animationDuration = `${5 + Math.random() * 10}s`;
flow.style.animationDelay = `${Math.random() * 8}s`;
flow.style.height = `${1 + Math.random() * 2}px`;
document.body.appendChild(flow);
}
}
// Initialize digital background with theme integration
document.addEventListener('DOMContentLoaded', function() {
// Сначала создаем элементы
createBinaryRain();
createFloatingCode();
createConnectionNodes();
createDataFlows();
// Затем настраиваем наблюдение за темой
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.attributeName === 'class') {
setTimeout(updateBackgroundForTheme, 100); // Небольшая задержка для применения стилей
}
});
});
observer.observe(document.body, {
attributes: true,
attributeFilter: ['class']
});
// Инициализируем цвета при загрузке
setTimeout(updateBackgroundForTheme, 500);
});
// Также обновляем тему при переключении
function toggleTheme() {
document.body.classList.toggle('dark-mode');
const btn = document.querySelector('.theme-toggle');
if (document.body.classList.contains('dark-mode')) {
btn.textContent = '☀️ Светлая тема';
localStorage.setItem('theme', 'dark');
} else {
btn.textContent = '🌙 Темная тема';
localStorage.setItem('theme', 'light');
}
// Обновляем фон после переключения темы
setTimeout(updateBackgroundForTheme, 100);
}
// Загрузка темы при загрузке страницы
document.addEventListener('DOMContentLoaded', function() {
const savedTheme = localStorage.getItem('theme');
const btn = document.querySelector('.theme-toggle');
if (savedTheme === 'dark') {
document.body.classList.add('dark-mode');
btn.textContent = '☀️ Светлая тема';
} else {
btn.textContent = '🌙 Темная тема';
}
});
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 MiB

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-brand-telegram"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M15 10l-4 4l6 6l4 -16l-18 7l4 2l2 6l3 -4" /></svg>

Before

Width:  |  Height:  |  Size: 364 B

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-brand-vk"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M14 19h-4a8 8 0 0 1 -8 -8v-5h4v5a4 4 0 0 0 4 4h0v-9h4v4.5l.03 0a4.531 4.531 0 0 0 3.97 -4.496h4l-.342 1.711a6.858 6.858 0 0 1 -3.658 4.789h0a5.34 5.34 0 0 1 3.566 4.111l.434 2.389h0h-4a4.531 4.531 0 0 0 -3.97 -4.496v4.5z" /></svg>

Before

Width:  |  Height:  |  Size: 538 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 329 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

-681
View File
@@ -1,681 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="keywords" content="
Fullstack-разработчик, Fullstack developer, Backend разработка, Frontend разработка, Веб-разработка, Программист Java, Программист Golang, Vue3.js разработка, JavaScript разработчик, Разработка веб-приложений, Создание сайтов, Микросервисная архитектура, REST API, PostgreSQL, Docker, Системное проектирование, Поиск тимейтов, Нетворкинг разработчиков, IT сообщество, Open-source проекты, Присоединиться к команде, Команда мечты, Рекрутинг разработчиков, Поиск программистов, Поиск дизайнеров, Поиск аналитиков, Удаленная команда, Профессиональный рост в IT, Совместная разработка, Технический предприниматель, Стартап партнерство, Инвестиции в IT, Соучредитель проекта, Бизнес-партнер, Tech Lead, Развитие проекта, Стратегическое партнерство, Венчурные инвестиции, Digital-продукты, Монетизация проектов, Travel Tech, Туристическая платформа, Планирование путешествий, Yalarba.ru, Туризм Башкортостан, Разработка платформы, Экосистема проектов, Маркетплейс туризма, Сайт-визитка разработчика, Портфолио программиста, Удаленная работа, Фриланс, Аутсорс разработка, Создание продукта с нуля, Agile разработка, Управление IT проектами, Цифровая трансформация
" />
<link rel="icon" href="./images/favicon/code_orange.png" />
<link rel="stylesheet" href="style.css" />
<script src="scripts.js"></script>
<script src="darkThemeToggle.js"></script>
<script src="digital_background.js"></script>
<script src="JavaScript/analytics.js"></script>
<title>ValitovGaziz - Предприниматель - Fullstack-разработчик</title>
</head>
<body>
<header class="hero">
<div class="hero-content">
<div class="hero-text">
<h1>ВАЛИТОВ ГАЗИЗ</h1>
<h3 class="hero-subtitle">
Технологический предриниматель & Fullstack-разработчик
</h3>
<p class="hero-description">
Создаю цифровое решение для отдыха. Развиваю проект
<strong>Yalarba.ru</strong> — платформу, которая меняет подход к
путешествиям по Башкортостану.
</p>
<div class="hero-buttons">
<button onclick="sendMessageTelegram()" class="btn btn-primary">
Обсудить сотрудничество
</button>
<button onclick="sendMessageTelegram()" class="btn btn-secondary">
Написать мне
</button>
<a href="blog.html" class="btn btn-secondary">
📝 Читать блог
</a>
</div>
</div>
</div>
<!-- Кнопка переключения темы -->
<button class="theme-toggle" onclick="toggleTheme()">
🌙 Темная тема
</button>
<div class="social_links_block">
<div class="social_link_block">
<h4>Подписывайтесь в ВК</h4>
<a href="https://vk.com" target="_blank">
<div class="social_link">
<img src="./images/favicon/brand-vk.svg" alt="VK - вконтакте" />
</div>
</a>
</div>
<div class="social_link_block">
<h4>Пишите в телеграм</h4>
<a href="https://t.me/valitovgaziz" target="_blank">
<div class="social_link">
<img src="./images/favicon/brand-telegram.svg" alt="телеграмм" />
</div>
</a>
</div>
</div>
</header>
<div class="section about">
<div class="about-valitovgaziz-photo-box">
<img src="./images/ValitovGaziz/valitovgaziz3.jpg" alt="Valitov Gaziz" id="valitovgaziz-photo-img"
loading="lazy" />
</div>
<div class="about-text">
<h2>Технический предприниматель и Fullstack-разработчик</h2>
<ul>
<li>г. Кумерау, 1985 год родиля</li>
<li>1992 - 2002 г. Кумертау, БРГИ 3</li>
<li>2002 - 2005 г.Уфа, УГАТУ, специальность "Сварочное производство"</li>
<li>2005 - 2009 Росстовская область, СКВО, служба в армии по контракту</li>
<li>2009 - 2012 г. Кумертау, станочник "Токарь-расточник" КумАПП</li>
<li>2012 -2015 село Старосубхангулово, ремонт электроники. ООО "БААС - сервис" владелец</li>
<li>2015 - 2020 г. Уфа, учеба в УКСиВТ "Техник по Информационным Системам"</li>
<li>с 2021 самообучние и работа над проектом Ял Арба, владелец</li>
</ul>
<div class="resume-block">
<a href="resume/resume.html" id="resume-link" target="_blank">resume</a>
</div>
<p>
Мой подход:
<strong>"Технологии как инструмент для решения реальных проблем"</strong>. Именно этот принцип лежит в основе
моего флагманского проекта
<a href="https://yalarba.ru" target="_blank">
Yalarba.ru
</a> <a href="https://easysite102.ru" target="_blank">
easysite102.ru
</a>
платформы, которая упрощает планирование путешествий и открывает новые
возможности для туризма.
</p>
<div class="entrepreneur-highlights">
<div class="highlight-item">
<h4>🎯 Техническое видение</h4>
<p>
Создаю архитектуру, которая масштабируется и адаптируется под
растущие потребности бизнеса
</p>
</div>
<div class="highlight-item">
<h4>💡 Бизнес-ориентация</h4>
<p>
Фокусируюсь на создании ценности для пользователей и устойчивых
бизнес-моделях
</p>
</div>
<div class="highlight-item">
<h4>🚀 Практический подход</h4>
<p>
От прототипа к продукту: быстрое тестирование гипотез и
итерационная разработка
</p>
</div>
<div class="highlight-item">
<h4>❤️‍🔥 Меня мотивирует</h4>
<p>
Процесс создания проекта с большой пользой многим людям - это то,
что по-настоящему подпитывает меня, давая энергию для ежедневного
стремления к лучшему будущему.
</p>
</div>
</div>
</div>
</div>
<!-- НОВАЯ СЕКЦИЯ: О репозитории -->
<div class="section repository">
<h2>
👨‍💻 О репозитории
<a href="https://github.com/valitovgaziz" class="link-style-none" target="_blank">
ValitovGaziz-GitHub.com
</a>
</h2>
<p>
Добро пожаловать! Этот репозиторий — моё цифровое портфолио и
пространство для экспериментов.
</p>
<div class="projects-grid">
<div class="project-card">
<h3>
🌐
<a href="https://valitovgaziz.ru" class="link-style-none" target="_blank">ValitovGaziz.ru</a>
</h3>
<p>
Сайт-визитка, который вы сейчас просматриваете. Здесь собрана
информация о моих навыках, проектах и способах связи.
</p>
</div>
<div class="project-card">
<h3>
🏞️
<a href="https://yalarba.ru" class="link-style-none" target="_blank">Yalarba.ru</a>
</h3>
<p>
Платформа для туризма по Башкортостану. Помогает путешественникам
открывать новые места и планировать маршруты.
</p>
</div>
<div class="project-card">
<h3>
🏃‍♂️
<a href="https://BegushiyBashkir.ru" class="link-style-none" target="_blank">BegushiyBashkir.ru</a>
</h3>
<p>
Сайт бегового клуба "Бегущий Башкир", основанного моим другом
<a href="https://t.me/zagir_aminev">Аминевым Загиром.</a>
</p>
</div>
</div>
<div class="current-info">
<h3>
Что сейчас в работе?
<a href="https://easysite102.ru" class="link-style-none" target="_blank"
title="Конструктор сайтов для туристических объектов">easysite102.ru</a>
</h3>
<ul>
<li>
<strong>Разрабатываю:</strong> easysite102.ru - как часть экосистемы
YalArba.ru.
</li>
<li>
<strong>Открыт к сотрудничеству:</strong> Участвую в разработке
open-source проектов.
</li>
<li><strong>Нужна помощь:</strong> В развитии моих проектов.</li>
</ul>
<p>
<strong>Задавайте вопросы</strong> по моим проектам или всему, в чём
могу быть полезен.
</p>
</div>
</div>
<!-- НОВАЯ СЕКЦИЯ: Команда мечты -->
<div class="section team-section">
<div class="team-header">
<h2>🚀 Ищем тимейтов для роста и прорыва</h2>
<p class="team-tagline">
Создаем digital-будущее вместе через разработку цифровых решений
</p>
</div>
<div class="team-content">
<div class="team-mission">
<h3>💫 Наша миссия</h3>
<p>
Мы строим сообщество профессионалов, которые через технологии
создают реальную пользу для людей. Это не коммерческая работа — это
возможность расти, решая сложные задачи и открывая новые горизонты.
</p>
</div>
<div class="team-roles">
<h3>👥 Кого мы ищем:</h3>
<div class="roles-grid">
<div class="role-card">
<h4>💻 Программисты</h4>
<p>
Fullstack, Backend, Frontend, Mobile — все, кто готов строить
масштабируемые решения
</p>
</div>
<div class="role-card">
<h4>🎨 Дизайнеры</h4>
<p>UI/UX, продуктовые дизайнеры, креативные мыслители</p>
</div>
<div class="role-card">
<h4>📊 Аналитики</h4>
<p>
Ищем аналитика (системного и бизнес‑аналитика) для анализа
процессов, сбора требований и перевода бизнес‑потребностей в
технические решения.
</p>
</div>
<div class="role-card">
<h4>🚀 Продавцы-стратеги</h4>
<p>Кто понимает, как digital-продукты меняют рынки</p>
</div>
</div>
</div>
<div class="team-value">
<h3>🎯 Что получаете взамен:</h3>
<ul>
<li>
<strong>Реальный опыт</strong> — задачи уровня коммерческих
проектов
</li>
<li>
<strong>Профессиональный рост</strong> — следующий уровень
навыков гарантирован
</li>
<li>
<strong>Нетворкинг</strong> — сообщество сильных специалистов
</li>
<li>
<strong>Портфолио</strong> — проекты, которые впечатляют
работодателей
</li>
<li>
<strong>Горизонтальное развитие</strong> — возможность
пробовать себя в смежных ролях
</li>
</ul>
</div>
<div class="team-challenge">
<h3>⚡ Уровень сложности:</h3>
<p>
Спектр задач достаточно высок — решая их, вы гарантированно
подниметесь на следующую ступень развития. Мы работаем с
технологиями, которые определяют будущее: микросервисы, AI/ML,
масштабируемые архитектуры, современный UX и бизнес-модели.
</p>
</div>
<div class="team-cta">
<h3>Готовы расти вместе?</h3>
<p>
Если вы ищете не просто проект, а сообщество для профессионального
прорыва — давайте знакомиться!
</p>
<button class="btn btn-primary" onclick="sendMessageTelegram()">
Присоединиться к команде
</button>
</div>
</div>
</div>
<!-- ОБНОВЛЕННАЯ СЕКЦИЯ: Yalarba -->
<div id="yalarba-invest" class="section yalarba-section">
<div class="yalarba-header">
<h2>
🚀 <a href="https://yalarba.ru" target="_blank">Yalarba.ru</a>
Travel Tech проект
</h2>
<p class="yalarba-tagline">
Платформа для планирования путешествий нового поколения
</p>
</div>
<div class="yalarba-content">
<div class="yalarba-stats">
<div class="stat">
<h3>❤️</h3>
<p>проект, рожденный от любви к краю</p>
</div>
<div class="stat">
<h3>🤝</h3>
<p>открыт для помощи и сотрудничества</p>
</div>
<div class="stat">
<h3>🚀</h3>
<p>готов к росту с правильной командой</p>
</div>
</div>
<div class="yalarba-value">
<h3>Технологический стек проекта:</h3>
<ul>
<li>✅ Микросервисная архитектура на Golang (Gorm, Chi)</li>
<li>✅ Современный фронтенд на Nuxt.js 4, Vue3.js</li>
<li>✅ Оптимизированная база данных PostgreSQL</li>
<li>
✅ Контейнеризация и легкое масштабирование через Docker, Docker
Swarm
</li>
<li>✅ Полный цикл разработки от идеи до продукта</li>
</ul>
</div>
<div class="investment-cta">
<h3>Инвестиционные возможности</h3>
<p>
Проект открыт для стратегических партнерств и инвестиций. Если вас
заинтересовала платформа, давайте обсудим перспективы
сотрудничества.
</p>
<button class="btn btn-primary" onclick="sendMessageTelegram()">
Обсудить детали
</button>
</div>
</div>
</div>
<div class="section">
<h2>Опыт работы</h2>
<div class="timeline">
<div class="timeline-item">
<h3>Основатель и Tech Lead - Yalarba.ru</h3>
<p><strong>2020 — настоящее время</strong> (5+ лет)</p>
<p>
Разработка и продвижение инновационной платформы для планирования
путешествий с полным циклом разработки:
</p>
<ul>
<li>Создание архитектуры микросервисов на Nuxt.js 4 и Golang</li>
<li>Разработка современного фронтенда на Nuxt.js 4 & Vue3.js</li>
<li>Проектирование и оптимизация баз данных PostgreSQL</li>
<li>Внедрение Docker и контейнеризации для масштабирования</li>
<li>Управление проектом, планирование развития продукта</li>
</ul>
</div>
<div class="timeline-item">
<h3>Fullstack-разработчик (Проектная работа)</h3>
<p><strong>2017 — настоящее время</strong> (7+ лет)</p>
<p>Участие в различных IT-проектах:</p>
<ul>
<li>Разработка лендинг-страниц и сайтов-визиток</li>
<li>Создание маркетплейсов и туристических агрегаторов</li>
<li>
Проектирование REST API на Golang (gorm, chi), PostgresQL, https
</li>
<li>Разработка фронтенда на Nuxt.js 4 (vue3.js)</li>
</ul>
</div>
</div>
</div>
<!-- Обновленная секция навыков в index.html -->
<div class="section">
<h2>Навыки</h2>
<div class="skills-container">
<div class="skill-card">
<div class="skill-header">
<h3 class="skill-name">Golang</h3>
<span class="skill-level advanced">Продвинутый</span>
</div>
<p class="skill-description">
Высокопроизводительные backend сервисы
</p>
<div class="skill-acquisition">
<strong>Опыт:</strong> 2+ лет коммерческой разработки, REST API,
best practices
</div>
<div class="skill-growth">
Concurrency patterns, advanced Go features
</div>
</div>
<div class="skill-card">
<div class="skill-header">
<h3 class="skill-name">JavaScript</h3>
<span class="skill-level advanced">Продвинутый</span>
</div>
<p class="skill-description">
Fullstack разработка, современный ES6+
</p>
<div class="skill-acquisition">
<strong>Опыт:</strong> 3+ лет коммерческой разработки, Vue.js,
Node.js
</div>
<div class="skill-growth">
TypeScript, advanced patterns, performance optimization
</div>
</div>
<div class="skill-card">
<div class="skill-header">
<h3 class="skill-name">Vue3</h3>
<span class="skill-level intermediate">Средний</span>
</div>
<p class="skill-description">
Современный фронтенд с Composition API
</p>
<div class="skill-acquisition">
<strong>Опыт:</strong> Разработка SPA приложений, Vue Router, Pinia
</div>
<div class="skill-growth">Vue 3 advanced patterns, testing, SSR</div>
</div>
<div class="skill-card">
<div class="skill-header">
<h3 class="skill-name">Nuxt</h3>
<span class="skill-level intermediate">Средний</span>
</div>
<p class="skill-description">SSR/SSG приложения на Vue.js</p>
<div class="skill-acquisition">
<strong>Опыт:</strong> Nuxt 3, server-side rendering, static site
generation
</div>
<div class="skill-growth">Nuxt 4, advanced caching strategies</div>
</div>
<div class="skill-card">
<div class="skill-header">
<h3 class="skill-name">PostgreSQL</h3>
<span class="skill-level intermediate">Средний</span>
</div>
<p class="skill-description">
Реляционные базы данных, оптимизация запросов
</p>
<div class="skill-acquisition">
<strong>Опыт:</strong> Проектирование схем, индексы, сложные запросы
</div>
<div class="skill-growth">
Advanced SQL, partitioning, replication
</div>
</div>
<div class="skill-card">
<div class="skill-header">
<h3 class="skill-name">Docker</h3>
<span class="skill-level intermediate">Средний</span>
</div>
<p class="skill-description">Контейнеризация приложений</p>
<div class="skill-acquisition">
<strong>Опыт:</strong> Docker Compose, multi-stage builds,
оптимизация образов
</div>
<div class="skill-growth">
Kubernetes, Docker Swarm, orchestration
</div>
</div>
<div class="skill-card">
<div class="skill-header">
<h3 class="skill-name">Java</h3>
<span class="skill-level beginner">Начинающий</span>
</div>
<p class="skill-description">
Backend разработка микросервисов и enterprise приложений
</p>
<div class="skill-acquisition">
<strong>Опыт:</strong> Коммерческая разработка 2+ лет, Spring
Framework, Hibernate
</div>
<div class="skill-growth">
Углубление в Spring Boot 3, reactive programming
</div>
</div>
<div class="skill-card">
<div class="skill-header">
<h3 class="skill-name">Spring Framework</h3>
<span class="skill-level beginner">Начинающий</span>
</div>
<p class="skill-description">
Создание масштабируемых enterprise приложений
</p>
<div class="skill-acquisition">
<strong>Опыт:</strong> Spring Boot, Spring Security, Spring Data,
Spring MVC
</div>
<div class="skill-growth">
Изучение Spring Cloud, микросервисная архитектура
</div>
</div>
</div>
</div>
<div class="section">
<h2>Образование</h2>
<div class="timeline">
<div class="timeline-item">
<h3>УКСИВТ</h3>
<p>Уфимский колледж статистики и информатики</p>
<p>Техник по информационным системам</p>
<p><strong>2016 - 2020</strong></p>
</div>
<div class="timeline-item">
<h3>
Автономная некоммерческая организация высшего образования
«Университет Иннополис»
</h3>
<p>Java enterprise, Java enterprise developer</p>
<p><strong>2021 - 2021</strong></p>
</div>
<div class="timeline-item">
<h3>МТИ - Московский технологический институт.</h3>
<p>Разработка программного обеспечения</p>
<p><strong>2025 - ></strong></p>
</div>
</div>
</div>
<div class="section">
<h2>Курсы и сертификаты</h2>
<ul>
<li>2024: Управление проектами (Skillbox, Эффективный руководитель)</li>
<li>2022: Java Full Stack Developer (JetBrains Academy)</li>
<li>2021: Java Enterprise developer (Университет Иннополис)</li>
<li>
2020: Управление по Agile: Scrum, Kanban, Lean (Нетология-групп)
</li>
<li>2019: English intermediate (Frog-school)</li>
</ul>
</div>
<div class="section">
<h2>Языки</h2>
<ul>
<li>Башкирский — Родной</li>
<li>Русский — C1 (Продвинутый)</li>
<li>Английский — B2 (Средне-продвинутый)</li>
</ul>
</div>
<div class="section">
<h2>Контакты</h2>
<p>
Всегда рад новым знакомствам и интересным предложениям о сотрудничестве.
</p>
<div class="contact-info">
<p>
📱 Телеграм:
<a href="https://t.me/valitovgaziz" target="_blank">@valitovgaziz</a>
</p>
<p>
📧 Email:
<a href="mailto:valitovgaziz@yandex.ru" target="_blank">valitovgaziz@yandex.ru</a>
</p>
<p>
📞 Телефон:
<a href="tel:+79625439343" target="_blank">+7(962)543-93-43</a>
</p>
</div>
<button id="saveContactBtn" onclick="saveContact()">
📇 Сохранить контакт
</button>
</div>
<footer>
<div class="footer-links">
<div class="footer-section">
<h4>Технологии:</h4>
<div class="two-column-grid">
<div class="footer-box">
<ul>
<li>FrontEnd:</li>
<li>BackEnd:</li>
<li>DataBase:</li>
</ul>
</div>
<div class="footer-box">
<ul>
<li>Vue3.js Nuxt.js</li>
<li>Golang (Gorm, Chi)</li>
<li>PostgresQL</li>
</ul>
</div>
</div>
</div>
<div class="footer-section">
<h4>Контакты:</h4>
<div class="two-column-grid">
<div class="footer-box">
<ul>
<li>Telegram:</li>
<li>Phone:</li>
<li>Email:</li>
</ul>
</div>
<div class="footer-box">
<ul>
<li>
<a href="https://t.me/valitovgaziz" target="_blank">@valitovgaziz</a>
</li>
<li>
<a href="tel:+79625439343" target="_blank">8 (962) 543-93-43</a>
</li>
<li>
<a href="mailto:valitovgaziz@yandex.ru" target="_blank">valitovgaziz@yandex.ru</a>
</li>
</ul>
</div>
</div>
</div>
<div class="footer-section">
<h4>Сообщество:</h4>
<div class="two-column-grid">
<div class="footer-box">
<ul>
<li>Telegram channel:</li>
<li>Telegram channel:</li>
<li>VK group:</li>
</ul>
</div>
<div class="footer-box">
<ul>
<li>
<a href="https://t.me/ValitovGaziz_Ufa" target="_blank">Мои новости</a>
</li>
<li>
<a href="https://t.me/+oYymS0r6qG9lYWJi" target="_blank">YalArba.ru team</a>
</li>
<li>
<a href="https://vk.com/club222248484?from=groups" target="_blank">ЯлАрба | Путевозитель</a>
</li>
</ul>
</div>
</div>
</div>
</div>
<div class="footer-end-text">
<p>Уфа Ufa Өфө 2025 © Created by Valitov Gaziz</p>
</div>
</footer>
</body>
</html>
@@ -1,261 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="./style/main.css" />
<title>Валитов Газиз Камилевич · Резюме</title>
</head>
<body>
<div class="resume-card">
<!-- header -->
<div class="header">
<h1 class="name">Валитов Газиз Камилевич</h1>
<div class="subhead">
<span class="subhead-item"><i>📅</i> 40 лет (27.10.1985)</span>
<span class="subhead-item"><i>📍</i> Уфа, готов к переезду/командировкам</span>
<span class="subhead-item"><i>📞</i> <a href="tel:+79625439343">+7 (962) 5439343</a></span>
<span class="subhead-item"><i>✉️</i> <a
href="mailto:valitovgaziz@gmail.com">valitovgaziz@yandex.ru</a></span>
<span class="subhead-item"><i>📱</i> telegram: @valitovgaziz</span>
</div>
<div style="margin-top: 0.5rem;">
<span class="badge">гражданство РФ</span>
<span class="badge">разрешение на работу РФ</span>
</div>
<div class="job-title">Программист / Руководитель проектов</div>
<div class="specialization-block">
<span>Специализации:</span>
<div class="spec-list">
<div class="spec-item">Руководитель проектов</div>
<div class="spec-item">CIO (Директор по ИТ)</div>
<div class="spec-item">Программист-разработчик</div>
<div class="spec-item">Руководитель группы разработки</div>
</div>
</div>
<div style="margin-top: 1rem; display: flex; flex-wrap: wrap; gap: 1rem 2rem;">
<span>✅ Полная занятость / частичная / проектная</span>
<span>✅ Полный день / сменный / гибкий / удалёнка</span>
<span>🚌 Время в пути до 1 часа</span>
</div>
</div>
<!-- main two column -->
<div class="grid-2">
<!-- left column: skills, languages, образование, водительские -->
<div class="sidebar">
<!-- Ключевые навыки (из списка) -->
<div class="section-title">Ключевые навыки</div>
<div class="skill-tags">
<span class="skill-tag">Английский B2</span><span class="skill-tag">Linux</span><span
class="skill-tag">Adobe Photoshop</span>
<span class="skill-tag">CorelDRAW</span><span class="skill-tag">C</span><span
class="skill-tag">Figma</span>
<span class="skill-tag">Git</span><span class="skill-tag">SQL</span><span
class="skill-tag">Agile</span>
<span class="skill-tag">Java</span><span class="skill-tag">ООП</span><span
class="skill-tag">Управление персоналом</span>
<span class="skill-tag">Atlassian Jira</span><span class="skill-tag">Spring Framework</span><span
class="skill-tag">JUnit</span>
<span class="skill-tag">PostgreSQL</span><span class="skill-tag">XPath</span><span
class="skill-tag">Go</span>
<span class="skill-tag">Intellij IDEA</span><span class="skill-tag">Spring MVC</span><span
class="skill-tag">MySQL</span>
<span class="skill-tag">Internet Marketing</span><span class="skill-tag">Грамотная речь</span>
<span class="skill-tag">Организаторские навыки</span><span class="skill-tag">Обучение
персонала</span>
<span class="skill-tag">Разработка ПО</span><span class="skill-tag">Agile Project Management</span>
</div>
<!-- Знание языков -->
<div class="section-title">Языки</div>
<div class="lang-item">
<span class="lang-name">Башкирский</span>
<span class="lang-level">родной</span>
</div>
<div class="lang-item">
<span class="lang-name">Русский</span>
<span class="lang-level">C1 —
продвинутый</span>
</div>
<div class="lang-item">
<span class="lang-name">Английский</span>
<span class="lang-level">B1 - средний</span>
</div>
<!-- Образование -->
<div class="section-title">Образование</div>
<div style="margin-bottom: 1rem;">
<div><strong style="color: #0b3b5c;">2020 · УКСИВТ</strong> — техник по информационным системам
</div>
<div style="margin-top: 0.5rem;"><strong style="color: #0b3b5c;">2004 · УГАТУ</strong>
Автоматизация технологических систем, сварочное производство (неоконч. высшее)</div>
</div>
<!-- курсы, повышение квалификации -->
<div class="section-title">Повышение квалификации</div>
<ul style="list-style-type: none; padding-left: 0;">
<li style="margin-bottom: 0.5rem;">🔹 <strong>2024</strong> Skillbox — Эффективный руководитель
(управление проектами)</li>
<li style="margin-bottom: 0.5rem;">🔹 <strong>2022</strong> JetBrains Academy — Java FullStack
Developer</li>
<li style="margin-bottom: 0.5rem;">🔹 <strong>2021</strong> Университет Иннополис — java-программист
</li>
<li style="margin-bottom: 0.5rem;">🔹 <strong>2020</strong> Нетология — Управление по Agile: Scrum,
Kanban, Lean</li>
<li style="margin-bottom: 0.5rem;">🔹 <strong>2019</strong> Frog-school — English Intermediate</li>
<li style="margin-bottom: 0.5rem;">🔹 <strong>2019</strong> Школа студия телерадио (ГУП ТРК
"Башкортостан") — телерадиоведущий</li>
</ul>
</div>
<!-- правый столбец: опыт работы и обо мне -->
<div class="main-content">
<!-- Опыт работы 12 лет 1 месяц -->
<div class="section-title">Опыт работы — 12 лет 1 месяц</div>
<!-- Август 2023 — Март 2024 -->
<div class="job-entry">
<div class="job-header">
<span class="job-company">ООО "ИКЦ Ял Арба"</span>
<span class="job-period">Ноябрь 2022 — настоящее время (30 мес)</span>
</div>
<div class="job-position">Директор</div>
<div class="job-desc">Наем персонала, руководитель группы разработки, маркетинг, продажи,
разрбаотка, написание кода.</div>
</div>
<!-- Август 2021 — Октябрь 2021 -->
<div class="job-entry">
<div class="job-header">
<span class="job-company">ИП Сафаров Я.Р., Уфа</span>
<span class="job-period">Авг 2021 — Окт 2021 (3 мес)</span>
</div>
<div class="job-position">Программист 1С</div>
<div class="job-desc">Разработка не типовых конфигураций для платформы 1С ERP.</div>
</div>
<!-- Апрель 2019 — Октябрь 2019 -->
<div class="job-entry">
<div class="job-header">
<span class="job-company">ГУП ТРК "Башкортостан" БСТ (СМИ)</span>
<span class="job-period">Апр 2019 — Окт 2019 (7 мес)</span>
</div>
<div class="job-position">Инженер</div>
<div class="job-desc">Звукозапись, обслуживание кинокамер. Командировки в районы в качестве
звукового оператора. Сопровождение и ведение записи на реал тайм проекте "Республика лайв".
</div>
</div>
<!-- Май 2017 — Июль 2017 -->
<div class="job-entry">
<div class="job-header">
<span class="job-company">ООО "ЭРУДИТ", Старосубхангулово</span>
<span class="job-period">Май 2017 — Июль 2017 (3 мес)</span>
</div>
<div class="job-position">Сетевой администратор</div>
<div class="job-desc"></div>
</div>
<!-- Февраль 2017 — Май 2017 (ПАО УМПО) -->
<div class="job-entry">
<div class="job-header">
<span class="job-company">ПАО "УМПО", Уфа</span>
<span class="job-period">Фев 2017 — Май 2017 (4 мес)</span>
</div>
<div class="job-position">Токарь-расточник (5 разряд)</div>
<div class="job-desc">Цех по производству деталей редуктора Ка-32. Обработка на
координатно-расточном станке 1964г. Квалитеты до 6, точность менее ±0.01мм, шероховатость 5
класс. Чтение чертежей и техпроцесса.</div>
</div>
<!-- Июнь 2016 — Октябрь 2016 ООО "ПФО Вертикаль" -->
<div class="job-entry">
<div class="job-header">
<span class="job-company">ООО "ПФО Вертикаль" (аутстафф), Уфа</span>
<span class="job-period">Июнь 2016 — Окт 2016 (5 мес)</span>
</div>
<div class="job-position">Токарь-расточник (5 разряд)</div>
<div class="job-desc">Инструментальный цех, ночные смены. Координатно-расточной станок, высокая
точность (до ±0.01мм), 6 квалитет.</div>
</div>
<!-- Май 2013 — Окт 2015 ООО "БААС-сервис" -->
<div class="job-entry">
<div class="job-header">
<span class="job-company">ООО "БААС-сервис", Старосубхангулово</span>
<span class="job-period">Май 2013 — Окт 2015 (2 года 6 мес)</span>
</div>
<div class="job-position">Директор (услуги населению: фото/видео, ремонт)</div>
<div class="job-desc">Управление персоналом (8 чел), учет наличности, отчетность ИФНС, обучение.
Софт, диагностика железа, заправка картриджей, фото/видеосъемка, дизайн.</div>
</div>
<!-- Март 2009 — Окт 2012 ОАО "КумаПП" -->
<div class="job-entry">
<div class="job-header">
<span class="job-company">ОАО "КумаПП", Кумертау</span>
<span class="job-period">Март 2009 — Окт 2012 (3 года 8 мес)</span>
</div>
<div class="job-position">Токарь-расточник (5 разряд)</div>
<div class="job-desc">Расточка деталей вертолета (втулки, качалки, рессоры). Координатно-расточные
работы, допуски до ±0.01мм, квалитет 6, шероховатость 5 класс.</div>
</div>
<!-- Авг 2008 — Окт 2008 ООО НОП "Мега-Щит" -->
<div class="job-entry">
<div class="job-header">
<span class="job-company">ООО НОП "Мега-Щит", Ханты-Мансийск</span>
<span class="job-period">Авг 2008 — Окт 2008 (3 мес)</span>
</div>
<div class="job-position">Охранник</div>
<div class="job-desc">КПП, контроль пропускного режима, досмотр.</div>
</div>
<!-- Май 2005 — Июль 2008 Вооруженные силы РФ -->
<div class="job-entry">
<div class="job-header">
<span class="job-company">Вооруженные силы РФ</span>
<span class="job-period">Май 2005 — Июль 2008 (3 года 3 мес)</span>
</div>
<div class="job-position">Командир отделения</div>
<div class="job-desc">Командование отделением.</div>
</div>
<!-- Обо мне -->
<div class="section-title">Обо мне</div>
<div class="about-text">
Программист, коммуникатор, компанейский человек, всегда за положительный движ.
</div>
<!-- Дополнительные строки из резюме: Уфа, гражданство и пр уже вверху -->
<div
style="font-size: 0.9rem; color: #1f3f55; margin-top: 1rem; background: #f1f7fd; padding: 0.8rem; border-radius: 12px;">
<span style="font-weight:600;">Дополнительно:</span> linux, Figma, SQL, Agile, Jira, Java, Spring,
Go, интернет-маркетинг, командная работа, менторство.
</div>
</div>
</div>
<!-- нижний колонтитул (страница 4) -->
<div style="background-color: #f4f9ff; padding: 1.5rem 2.5rem; border-top: 1px solid #bfd5e6;">
<div style="display: flex; flex-wrap: wrap; gap: 2rem; justify-content: space-between;">
<div>
<span style="font-weight:600;">🏠 Проживание:</span> Уфа · готов к переезду / командировкам
</div>
<div>
<span style="font-weight:600;">📄 обновлено:</span> февраль 2026 года
</div>
<div>
<span style="font-weight:600;">📞 +7 (962) 543 - 93 - 43</span>
</div>
</div>
</div>
</div>
</body>
</html>
@@ -1,297 +0,0 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background-color: #eef2f5;
font-family: 'Segoe UI', Roboto, system-ui, -apple-system, sans-serif;
line-height: 1.5;
color: #1a2634;
padding: 2rem 1rem;
}
.resume-card {
max-width: 1100px;
margin: 0 auto;
background-color: #ffffff;
box-shadow: 0 10px 25px rgba(0, 35, 70, 0.1);
border-radius: 12px;
overflow: hidden;
border-top: 6px solid #0b3b5c;
}
/* header section */
.header {
background-color: #f9fcff;
padding: 2rem 2.5rem 1.5rem 2.5rem;
border-bottom: 1px solid #d9e4ed;
}
.name {
font-size: 2.8rem;
font-weight: 500;
letter-spacing: -0.5px;
color: #0b3b5c;
margin-bottom: 0.5rem;
line-height: 1.2;
}
.subhead {
display: flex;
flex-wrap: wrap;
gap: 1.5rem 2.5rem;
margin-top: 0.75rem;
color: #2f4858;
font-size: 1rem;
}
.subhead-item {
display: flex;
align-items: center;
gap: 6px;
}
.subhead-item i {
width: 20px;
color: #1e5a7a;
font-weight: 400;
opacity: 0.8;
}
.badge {
background-color: #e1edf7;
color: #0b3b5c;
padding: 0.3rem 1rem;
border-radius: 30px;
font-size: 0.85rem;
font-weight: 600;
display: inline-block;
margin-right: 10px;
margin-bottom: 6px;
border: 1px solid #b9d1e4;
}
.job-title {
font-size: 1.8rem;
font-weight: 400;
color: #1d4e6b;
margin: 1rem 0 0.5rem 0;
border-bottom: 2px solid #b0c8da;
padding-bottom: 0.5rem;
}
.specialization-block {
background: #e9f0f7;
padding: 1rem 2rem;
border-radius: 40px;
margin: 0.5rem 0 0 0;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 1rem;
}
.specialization-block span {
font-weight: 600;
color: #0b3b5c;
margin-right: 0.5rem;
}
.spec-list {
display: flex;
flex-wrap: wrap;
gap: 0.6rem 1.5rem;
}
.spec-item {
color: #1e3b4f;
border-left: 3px solid #1e5a7a;
padding-left: 10px;
font-size: 0.98rem;
}
.grid-2 {
display: grid;
grid-template-columns: 1fr 2.2fr;
gap: 2rem;
}
/* sidebar */
.sidebar {
background-color: #f7fafd;
padding: 2rem 1.8rem;
border-right: 1px solid #cddeec;
}
.main-content {
padding: 2rem 2rem 2rem 0.5rem;
}
.section-title {
font-size: 1.35rem;
font-weight: 600;
color: #0b3b5c;
border-bottom: 2px solid #b6d0e2;
padding-bottom: 6px;
margin: 1.8rem 0 1.2rem 0;
letter-spacing: -0.2px;
}
.section-title:first-of-type {
margin-top: 0;
}
.info-row {
margin-bottom: 0.75rem;
display: flex;
align-items: baseline;
}
.info-label {
width: 100px;
font-weight: 500;
color: #1e5a7a;
}
.info-value {
color: #152b39;
}
.skill-tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 0.5rem;
}
.skill-tag {
background: white;
border: 1px solid #b0c5d6;
padding: 0.25rem 1rem;
border-radius: 30px;
font-size: 0.85rem;
color: #0f3a52;
font-weight: 500;
box-shadow: 0 1px 2px rgba(0, 20, 40, 0.05);
}
.lang-item {
display: flex;
justify-content: space-between;
border-bottom: 1px dashed #b8cbd9;
padding: 0.5rem 0;
}
.lang-name {
font-weight: 600;
color: #0f3f5c;
}
.lang-level {
color: #1d5b81;
font-style: italic;
}
/* опыт */
.job-entry {
margin-bottom: 1.8rem;
}
.job-header {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: baseline;
margin-bottom: 0.25rem;
}
.job-company {
font-size: 1.2rem;
font-weight: 700;
color: #0b3b5c;
}
.job-period {
background: #dbe7f2;
padding: 0.2rem 1rem;
border-radius: 30px;
font-size: 0.75rem;
font-weight: 600;
color: #103a52;
letter-spacing: 0.3px;
}
.job-position {
font-weight: 600;
color: #1f5777;
margin: 0.15rem 0 0.4rem 0;
font-size: 1.05rem;
}
.job-desc {
color: #1e333f;
font-size: 0.93rem;
margin-left: 0.2rem;
padding-left: 0.8rem;
border-left: 3px solid #9bb7d0;
white-space: pre-line;
}
.compact-mb {
margin-bottom: 0.3rem;
}
hr {
border: 0;
border-top: 1px solid #c9dae7;
margin: 1rem 0;
}
.about-text {
background: #f0f6fc;
padding: 1.5rem 2rem;
border-radius: 50px 8px 50px 8px;
color: #103c58;
font-size: 1.1rem;
border-left: 6px solid #1f6a92;
margin: 2rem 0 1rem;
}
.footer-note {
font-size: 0.8rem;
color: #5e778a;
text-align: right;
border-top: 1px solid #b9cfdf;
padding-top: 0.8rem;
margin-top: 1rem;
}
a {
color: #1b5f89;
text-decoration: none;
border-bottom: 1px dotted #8eb1c7;
}
a:hover {
border-bottom: 2px solid #0b3b5c;
}
@media (max-width: 750px) {
.grid-2 {
grid-template-columns: 1fr;
}
.main-content {
padding: 2rem 1.5rem;
}
.sidebar {
border-right: 0;
}
.name {
font-size: 2.2rem;
}
}
-132
View File
@@ -1,132 +0,0 @@
function saveContact() {
// Создаем содержимое vCard (VCF)
const vCardData = `BEGIN:VCARD
VERSION:3.0
FN:Валитов Газиз Камилевич
N:Валитов;Газиз;Камилевич
ORG:FREELANCE
TITLE:FULLSTACK_DEVELOPER
TEL;TYPE=MOBILE:+79279238823
TEL;TYPE=MOBILE:+79044513441
TEL;TYPE=MOBILE:+79625439243
EMAIL;TYPE=HOME:valitovgaziz@gmail.com
EMAIL;TYPE=WORK:valitovgaziz@yandex.ru
URL:https://valitovgaziz.ru
URL:https://t.me/valitovgaziz
URL:https://vk.ru/id378105199
BDAY:1985-10-27
END:VCARD`;
// Создаем Blob (бинарный объект) с данными vCard
const blob = new Blob([vCardData], { type: 'text/vcard' });
// Создаем URL для скачивания
const url = URL.createObjectURL(blob);
// Создаем временную ссылку для скачивания
const link = document.createElement('a');
link.href = url;
link.download = 'valitovgaziz.vcf'; // Имя файла
link.click();
// Освобождаем память
URL.revokeObjectURL(url);
}
function loadTermSheet() {
// Create a temporary anchor element
const link = document.createElement('a');
// Set correct relative path to the PDF file
link.href = './assets/docs/TermSheet.pdf';
// Set download attribute with filename
link.download = 'TermSheet.pdf';
// Append to body to make it work in some browsers
document.body.appendChild(link);
// Trigger the download
link.click();
// Clean up
document.body.removeChild(link);
}
// Обработчик для кнопки "Запросить презентацию"
function sendMessageTelegram() {
// Проверяем, поддерживает ли браузер диалоги
if (typeof window.orientation !== 'undefined' && !window.confirm) {
// Для мобильных браузеров без поддержки prompt - открываем Telegram напрямую
window.open('https://t.me/valitovgaziz', '_blank');
return;
}
const message = prompt("Опишите, пожалуйста, ваше предложение или вопрос. Я свяжусь с вами в ближайшее время:");
if (message) {
const BOT_TOKEN = "8470085635:AAEPZcsN3n-3FkMdr7DzxbiQ3q8mXZTGwug";
const CHAT_ID = "559861569";
// Используем FormData вместо JSON (более надежно)
const formData = new FormData();
formData.append('chat_id', CHAT_ID);
formData.append('text', `📥 Новое сообщение с сайта ValitovGaziz:\n\n${message}`);
formData.append('parse_mode', 'HTML');
// Альтернативный URL
fetch(`https://api.telegram.org/bot${BOT_TOKEN}/sendMessage`, {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.ok) {
alert("Сообщение успешно отправлено! Я свяжусь с вами в ближайшее время.");
} else {
console.error('Telegram API Error:', data);
alert("Ошибка: " + (data.description || 'Неизвестная ошибка'));
}
})
.catch(error => {
console.error("Ошибка:", error);
alert("Произошла ошибка сети. Попробуйте позже или свяжитесь со мной напрямую.");
});
}
}
// Универсальный обработчик для кнопок
function setupButtonHandlers() {
const buttons = document.querySelectorAll('button[onclick*="sendMessageTelegram"]');
buttons.forEach(button => {
// Удаляем старые обработчики
button.removeAttribute('onclick');
// Добавляем универсальные обработчики
button.addEventListener('click', handleTelegramButtonClick);
button.addEventListener('touchstart', handleTelegramButtonClick, { passive: true });
});
}
// Обработчик кликов для Telegram кнопок
function handleTelegramButtonClick(event) {
event.preventDefault();
event.stopPropagation();
// Для touch-событий, предотвращаем повторное срабатывание
if (event.type === 'touchstart') {
const now = Date.now();
if (this.lastTouch && (now - this.lastTouch) < 500) {
return;
}
this.lastTouch = now;
}
sendMessageTelegram();
}
// Инициализация при загрузке страницы
document.addEventListener('DOMContentLoaded', function () {
setupButtonHandlers();
});
-464
View File
@@ -1,464 +0,0 @@
@import url("./style/digital_background.css");
@import url("./style/saveContactsButtonStyle.css");
@import url("./style/darkTheme.css");
@import url("./style/about.css");
@import url("./style/social_link.css");
@import url("./style/hero_section.css");
@import url("./style/yalarba_investmen.css");
@import url("./style/footer.css");
@import url("./style/repository_section.css");
@import url("./style/links_style.css");
@import url("./style/skill_section.css");
/* style.css - обновленный */
:root {
--primary: #a9e299;
--secondary: #63c1ff;
--light: #ecf0f1;
--dark: #36304d;
--success: #2ecc71;
--border-radius: 12px;
--shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
--transition: all 0.3s ease;
}
* {
transition: background-color 0.3s ease, color 0.3s ease,
border-color 0.3s ease;
}
html {
margin: 0;
padding: 0;
width: 100%;
scroll-behavior: smooth;
}
a {
text-decoration: none;
}
body {
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: #333;
min-width: 300px;
max-width: 1200px;
margin: 10px auto 5px auto;
padding: 10px 20px;
display: grid;
grid-template-rows: auto 1fr auto;
min-height: 100vh;
}
/* Улучшенная сетка для header */
header {
background-color: var(--primary);
color: var(--dark);
padding: 2rem 1rem;
text-align: center;
margin-bottom: 2rem;
border-radius: var(--border-radius);
display: grid;
grid-template-columns: 1fr auto;
grid-template-rows: auto auto;
gap: 1rem;
align-items: center;
position: relative;
}
.theme-toggle {
grid-column: 2;
grid-row: 1;
justify-self: end;
padding: 8px 12px;
background: var(--secondary);
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: var(--transition);
z-index: 1000;
}
.social_links_block {
grid-column: 1 / -1;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 1rem;
justify-items: center;
margin-top: 1rem;
}
/* Улучшенная сетка для секций */
.section {
background: rgb(226, 240, 241);
padding: 2rem;
margin-bottom: 2rem;
border-radius: var(--border-radius);
box-shadow: var(--shadow);
display: grid;
grid-template-columns: 1fr;
gap: 1.5rem;
}
.entrepreneur-highlights {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
}
.highlight-item {
background: rgba(255, 255, 255, 0.7);
padding: 1.5rem;
border-radius: var(--border-radius);
box-shadow: var(--shadow);
transition: var(--transition);
}
/* Сетка для команды */
.team-section {
display: grid;
grid-template-columns: 1fr;
gap: 2rem;
}
.team-header {
text-align: center;
}
.roles-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
}
.role-card {
background: rgba(255, 255, 255, 0.7);
padding: 1.5rem;
border-radius: var(--border-radius);
box-shadow: var(--shadow);
transition: var(--transition);
display: grid;
grid-template-rows: auto 1fr;
gap: 1rem;
}
.role-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.1);
}
/* Сетка для Yalarba секции */
.yalarba-section {
display: grid;
grid-template-columns: 1fr;
gap: 2rem;
}
.yalarba-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 1rem;
justify-items: center;
}
.yalarba-value ul {
display: grid;
grid-template-columns: 1fr;
gap: 0.5rem;
}
/* Сетка для контактов */
.contact-info {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
}
/* Сетка для футера */
footer {
display: grid;
grid-template-columns: 1fr;
gap: 2rem;
text-align: center;
padding: 1em 0 0 0;
color: var(--dark);
font-size: 0.9rem;
border-radius: 1rem;
}
.footer-links {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2rem;
padding: 1em;
}
.footer-section {
display: grid;
grid-template-columns: 1fr;
gap: 0.5rem;
}
.two-column-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
border-left: 1px solid black;
grid-template-rows: auto; /* Явно указываем одну строку */
grid-auto-flow: row; /* Запрещаем автоматическое создание новых строк */
}
/* Кнопки */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.8rem 1.5rem;
border-radius: 5px;
text-decoration: none;
font-weight: bold;
transition: var(--transition);
border: none;
cursor: pointer;
text-align: center;
}
.btn-primary {
background-color: var(--secondary);
color: white;
}
.btn-primary:hover {
background-color: #2980b9;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.btn-secondary {
color: white;
}
.btn-secondary:hover {
background-color: white;
color: var(--primary);
}
/* Timeline улучшения */
.timeline {
display: grid;
grid-template-columns: 1fr;
gap: 2rem;
position: relative;
padding-left: 30px;
}
.timeline:before {
content: "";
position: absolute;
left: 10px;
top: 0;
bottom: 0;
width: 2px;
background: var(--secondary);
}
.timeline-item {
position: relative;
display: grid;
grid-template-columns: 1fr;
gap: 0.5rem;
}
.timeline-item:before {
content: "";
position: absolute;
left: -30px;
top: 5px;
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--primary);
border: 2px solid var(--secondary);
}
/* Адаптация для мелких экранов */
@media (max-width: 768px) {
body {
padding: 5px 10px;
margin: 5px auto;
}
header {
grid-template-columns: 1fr;
grid-template-rows: auto auto auto;
padding: 1.5rem 1rem;
}
.theme-toggle {
grid-column: 1;
grid-row: 1;
justify-self: center;
margin-bottom: 1rem;
}
.section {
padding: 1.5rem;
}
.about {
grid-template-columns: 1fr;
}
.about-valitovgaziz-photo-box img {
min-width: 100%;
}
.about-text {
width: 100%;
}
.projects-grid {
grid-template-columns: 1fr;
}
.roles-grid {
grid-template-columns: 1fr;
}
.yalarba-stats {
grid-template-columns: repeat(2, 1fr);
}
.footer-links {
grid-template-columns: 1fr;
}
.two-column-grid {
border-left: none;
border-top: 1px solid black;
padding-top: 1rem;
}
.timeline {
padding-left: 20px;
}
.timeline:before {
left: 8px;
}
.timeline-item:before {
left: -20px;
}
}
/* Адаптация для очень маленьких экранов */
@media (max-width: 480px) {
body {
padding: 5px;
}
.section {
padding: 1rem;
}
.hero {
padding: 1.5rem 1rem;
}
.hero-subtitle {
font-size: 1.2rem;
}
.hero-description {
font-size: 1rem;
}
.projects-grid,
.roles-grid,
.entrepreneur-highlights {
grid-template-columns: 1fr;
}
.yalarba-stats {
grid-template-columns: 1fr;
}
.skills-container {
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
}
}
/* Улучшения для планшетов */
@media (min-width: 769px) and (max-width: 1024px) {
.about {
grid-template-columns: 1fr;
gap: 2rem;
}
.projects-grid {
grid-template-columns: repeat(2, 1fr);
}
.roles-grid {
grid-template-columns: repeat(2, 1fr);
}
.entrepreneur-highlights {
grid-template-columns: repeat(2, 1fr);
}
}
/* Анимации и улучшения UX */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.section {
animation: fadeIn 0.5s ease-out;
}
.project-card,
.role-card,
.highlight-item {
animation: fadeIn 0.5s ease-out;
}
/* Улучшения доступности */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
/* Улучшения фокуса для доступности */
button:focus,
a:focus {
outline: 2px solid var(--secondary);
outline-offset: 2px;
}
/* Улучшения для темной темы */
body.dark-mode .highlight-item,
body.dark-mode .role-card {
background: var(--dark-card);
}
body.dark-mode .project-card:hover,
body.dark-mode .role-card:hover,
body.dark-mode .highlight-item:hover {
transform: translateY(-5px);
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.3);
}
-52
View File
@@ -1,52 +0,0 @@
.about {
display: flex;
width: inherit;
height: auto;
flex-direction: column;
flex-wrap: wrap;
justify-content: center;
align-items: center;
}
.about-valitovgaziz-photo-box {
width: fit-content;
height: fit-content;
@media (max-width: 768px) {
width: 100%;
}
}
.about-valitovgaziz-photo-box img {
width: 100%;
height: auto;
object-fit: cover;
border-radius: 1em;
-webkit-box-shadow: 4px 4px 8px 9px rgba(34, 60, 80, 0.2);
-moz-box-shadow: 4px 4px 8px 9px rgba(34, 60, 80, 0.2);
box-shadow: 4px 4px 8px 9px rgba(34, 60, 80, 0.2);
}
#about-valitovgaziz-photo-img {
max-width: 100%;
height: auto;
}
/* Сетка для about секции */
.about {
display: grid;
grid-template-columns: 1fr;
gap: 2rem;
align-items: start;
}
.about-valitovgaziz-photo-box {
display: grid;
grid-template-columns: 1fr;
justify-self: center;
}
.about-text {
display: grid;
grid-template-columns: 1fr;
gap: 1.5rem;
}
-617
View File
@@ -1,617 +0,0 @@
/* Базовые стили для блога */
:root {
--primary: #9ab09492;
--secondary: #3498db;
--accent: #2ecc71;
--light: #f8f9fa;
--dark: #1a252f;
--gray: #6c757d;
--light-gray: #e9ecef;
--border-radius: 12px;
--shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
--shadow-hover: 0 8px 15px rgba(0, 0, 0, 0.15);
--transition: all 0.3s ease;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
line-height: 1.6;
color: var(--dark);
background-color: var(--light);
transition: var(--transition);
min-height: 100vh;
}
/* Стили для темной темы */
body.dark-mode {
background-color: #121212;
color: #e0e0e0;
}
body.dark-mode .blog-nav {
background-color: #1e1e1e;
border-bottom: 1px solid #333;
}
body.dark-mode .blog-header {
background: linear-gradient(135deg, #2c3e50 0%, #1a252f 100%);
}
body.dark-mode .blog-container,
body.dark-mode .blog-sidebar,
body.dark-mode .blog-post {
background-color: #1e1e1e;
color: #e0e0e0;
}
body.dark-mode .blog-sidebar-section,
body.dark-mode .blog-post-content,
body.dark-mode .blog-post-footer {
border-color: #333;
}
body.dark-mode .blog-quote {
background-color: #2d2d2d;
border-left-color: var(--secondary);
}
body.dark-mode .blog-tag {
background-color: #2c3e50;
color: #e0e0e0;
}
body.dark-mode .blog-pagination-btn {
background-color: #2c3e50;
color: #e0e0e0;
}
body.dark-mode .blog-footer {
background-color: #1a252f;
color: #b0b0b0;
}
/* Кнопка переключения темы */
.theme-toggle {
position: fixed;
top: 20px;
right: 20px;
z-index: 1000;
padding: 8px 16px;
background-color: var(--secondary);
color: white;
border: none;
border-radius: 20px;
cursor: pointer;
font-size: 14px;
transition: var(--transition);
box-shadow: var(--shadow);
}
.theme-toggle:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-hover);
}
/* Навигация */
.blog-nav {
background-color: white;
border-bottom: 1px solid var(--light-gray);
padding: 1rem 0;
position: sticky;
top: 0;
z-index: 100;
transition: var(--transition);
}
.blog-nav-container {
max-width: 1200px;
margin: 0 auto;
padding: 0 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.blog-nav-logo {
font-size: 1.5rem;
font-weight: bold;
color: var(--dark);
text-decoration: none;
transition: var(--transition);
}
.blog-nav-link {
color: var(--secondary);
text-decoration: none;
font-weight: 500;
transition: var(--transition);
}
.blog-nav-link:hover {
color: var(--dark);
}
/* Заголовок блога */
.blog-header {
background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%);
padding: 4rem 2rem;
text-align: center;
color: white;
}
.blog-header-content {
max-width: 1200px;
margin: 0 auto;
}
.blog-title {
font-size: 3rem;
margin-bottom: 1rem;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
}
.blog-subtitle {
font-size: 1.2rem;
margin-bottom: 2rem;
opacity: 0.9;
}
.blog-meta {
display: flex;
justify-content: center;
gap: 2rem;
flex-wrap: wrap;
}
.blog-meta-item {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.9rem;
}
/* Основной контейнер - ИСПРАВЛЕНЫ ПРОПОРЦИИ */
.blog-container {
max-width: 1200px;
margin: 3rem auto;
padding: 0 2rem;
display: grid;
/* Основная колонка 70%, боковая 30% */
grid-template-columns: 1fr 280px;
gap: 2.5rem;
}
/* Боковая панель - компактная */
.blog-sidebar {
position: sticky;
top: 100px;
height: fit-content;
width: 100%;
}
.blog-sidebar-section {
background-color: white;
border-radius: var(--border-radius);
padding: 1.25rem;
margin-bottom: 1.25rem;
box-shadow: var(--shadow);
transition: var(--transition);
width: 100%;
}
.blog-sidebar-section h3 {
margin-bottom: 0.75rem;
color: var(--dark);
font-size: 1.1rem;
}
.blog-sidebar-section p {
font-size: 0.9rem;
line-height: 1.5;
}
.blog-categories {
list-style: none;
}
.blog-categories li {
margin-bottom: 0.4rem;
}
.blog-category {
display: block;
padding: 0.4rem 0.75rem;
color: var(--gray);
text-decoration: none;
border-radius: 6px;
transition: var(--transition);
font-size: 0.9rem;
}
.blog-category:hover {
background-color: var(--light-gray);
color: var(--dark);
}
.blog-recent {
list-style: none;
}
.blog-recent li {
margin-bottom: 0.8rem;
padding-bottom: 0.8rem;
border-bottom: 1px solid var(--light-gray);
}
.blog-recent li:last-child {
margin-bottom: 0;
padding-bottom: 0;
border-bottom: none;
}
.blog-recent a {
color: var(--dark);
text-decoration: none;
transition: var(--transition);
font-size: 0.9rem;
display: block;
}
.blog-recent a:hover {
color: var(--secondary);
}
/* Основное содержание - шире */
.blog-content {
display: flex;
flex-direction: column;
gap: 2.5rem;
width: 100%;
}
.blog-post {
background-color: white;
border-radius: var(--border-radius);
overflow: hidden;
box-shadow: var(--shadow);
transition: var(--transition);
width: 100%;
}
.blog-post:hover {
transform: translateY(-5px);
box-shadow: var(--shadow-hover);
}
.blog-post-header {
padding: 2rem 2rem 1rem;
}
.blog-post-category {
display: inline-block;
padding: 0.25rem 0.75rem;
background-color: var(--light-gray);
color: var(--gray);
border-radius: 20px;
font-size: 0.8rem;
font-weight: 500;
margin-bottom: 1rem;
}
.blog-post-title {
font-size: 1.8rem;
margin-bottom: 1rem;
color: var(--dark);
line-height: 1.3;
}
.blog-post-meta {
display: flex;
align-items: center;
gap: 0.5rem;
color: var(--gray);
font-size: 0.9rem;
}
.blog-post-content {
padding: 0 2rem 1rem;
border-bottom: 1px solid var(--light-gray);
}
.blog-post-content p {
margin-bottom: 1rem;
font-size: 1.05rem;
line-height: 1.7;
}
.blog-post-content h3 {
margin: 1.5rem 0 1rem;
color: var(--dark);
font-size: 1.4rem;
}
.blog-post-content ul,
.blog-post-content ol {
margin: 1rem 0 1.5rem 1.5rem;
padding-left: 0.5rem;
}
.blog-post-content li {
margin-bottom: 0.75rem;
font-size: 1.05rem;
line-height: 1.6;
}
.blog-quote {
margin: 1.5rem 0;
padding: 1.5rem;
background-color: var(--light-gray);
border-left: 4px solid var(--secondary);
font-style: italic;
border-radius: 0 var(--border-radius) var(--border-radius) 0;
font-size: 1.1rem;
line-height: 1.6;
}
.blog-post-footer {
padding: 1.5rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 1rem;
}
.blog-post-tags {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.blog-tag {
display: inline-block;
padding: 0.25rem 0.75rem;
background-color: var(--light-gray);
color: var(--gray);
border-radius: 20px;
font-size: 0.8rem;
text-decoration: none;
transition: var(--transition);
}
.blog-tag:hover {
background-color: var(--secondary);
color: white;
}
.blog-comment-btn {
padding: 0.5rem 1rem;
background-color: var(--secondary);
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 0.9rem;
transition: var(--transition);
display: flex;
align-items: center;
gap: 0.5rem;
white-space: nowrap;
}
.blog-comment-btn:hover {
background-color: #2980b9;
transform: translateY(-2px);
}
/* Пагинация */
.blog-pagination {
max-width: 1200px;
margin: 3rem auto 4rem;
padding: 0 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.blog-pagination-btn {
padding: 0.75rem 1.5rem;
background-color: white;
color: var(--dark);
text-decoration: none;
border-radius: var(--border-radius);
transition: var(--transition);
border: 1px solid var(--light-gray);
display: flex;
align-items: center;
gap: 0.5rem;
}
.blog-pagination-btn:hover {
background-color: var(--secondary);
color: white;
border-color: var(--secondary);
}
.blog-pagination-current {
color: var(--gray);
}
/* Футер */
.blog-footer {
background-color: var(--dark);
color: white;
padding: 3rem 0;
margin-top: 4rem;
}
.blog-footer-content {
max-width: 1200px;
margin: 0 auto;
padding: 0 2rem;
text-align: center;
}
.blog-footer-content p {
margin-bottom: 1rem;
}
.blog-footer-content a {
color: var(--light-gray);
text-decoration: none;
transition: var(--transition);
}
.blog-footer-content a:hover {
color: var(--secondary);
}
/* Кнопка для мобильного меню */
.blog-sidebar-toggle {
display: none;
width: 100%;
padding: 1rem;
background-color: var(--secondary);
color: white;
border: none;
border-radius: var(--border-radius);
margin-bottom: 1rem;
cursor: pointer;
font-weight: 500;
}
/* Адаптивность */
@media (max-width: 1100px) {
.blog-container {
grid-template-columns: 1fr 240px;
gap: 2rem;
}
}
@media (max-width: 992px) {
.blog-container {
grid-template-columns: 1fr;
gap: 2rem;
}
.blog-sidebar {
position: static;
max-width: 600px;
margin: 0 auto;
}
}
@media (max-width: 768px) {
.blog-title {
font-size: 2.5rem;
}
.blog-container {
padding: 0 1.5rem;
}
.blog-nav-container {
padding: 0 1.5rem;
}
.blog-header {
padding: 3rem 1.5rem;
}
.blog-meta {
flex-direction: column;
gap: 1rem;
}
.blog-post-header,
.blog-post-content,
.blog-post-footer {
padding: 1.5rem;
}
.blog-post-title {
font-size: 1.6rem;
}
.blog-pagination {
flex-direction: column;
gap: 1rem;
text-align: center;
}
.blog-sidebar-toggle {
display: block;
}
.blog-sidebar {
display: none;
max-width: 100%;
}
.blog-sidebar.show {
display: block;
}
}
@media (max-width: 480px) {
.blog-title {
font-size: 2rem;
}
.blog-post-title {
font-size: 1.4rem;
}
.blog-container {
padding: 0 1rem;
}
.blog-post-header,
.blog-post-content,
.blog-post-footer {
padding: 1.25rem;
}
.blog-post-footer {
flex-direction: column;
align-items: flex-start;
gap: 1rem;
}
.blog-quote {
padding: 1rem;
font-size: 1rem;
}
}
/* Улучшения для темной темы */
body.dark-mode .blog-sidebar-section {
background-color: #2d2d2d;
}
body.dark-mode .blog-category {
color: #b0b0b0;
}
body.dark-mode .blog-category:hover {
background-color: #3d3d3d;
color: white;
}
body.dark-mode .blog-recent a {
color: #b0b0b0;
}
body.dark-mode .blog-recent a:hover {
color: var(--secondary);
}
@@ -1,246 +0,0 @@
/* Переменные для темной темы */
:root {
--dark-bg: #1a252f;
--dark-text: #ecf0f1;
--dark-card: #2c3e50;
--dark-border: #34495e;
--dark-secondary: #2980b9;
}
/* Кнопка переключения темы */
header {
position: relative;
}
/* В darkTheme.css - добавьте системные предпочтения */
@media (prefers-color-scheme: dark) {
:root {
--dark-bg: #0a0a0a;
--dark-text: #e0e0e0;
}
}
.theme-toggle {
position: absolute;
top: 10px;
right: 10px;
padding: 8px 12px;
background: var(--secondary);
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s ease;
z-index: 1000;
}
.theme-toggle:hover {
background: var(--dark-secondary);
transform: translateY(-2px);
}
/* Стили для темной темы */
body.dark-mode {
background-color: var(--dark-bg);
color: var(--dark-text);
}
body.dark-mode header {
background-color: var(--dark-bg);
color: var(--dark-text);
background: linear-gradient(135deg, var(--dark-bg) 0%, #1a535c 100%);
}
body.dark-mode .section {
background: var(--dark-card);
color: var(--dark-text);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3);
border: 1px solid var(--dark-border);
}
body.dark-mode .contact-info a {
color: var(--dark-secondary);
}
body.dark-mode .skill-tag {
background-color: var(--dark-border);
color: var(--dark-text);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.4);
}
body.dark-mode footer {
color: var(--dark-text);
background-color: var(--dark-card);
}
body.dark-mode h1,
body.dark-mode h2,
body.dark-mode h3 {
color: var(--dark-secondary);
}
body.dark-mode .project-link {
color: var(--dark-secondary);
}
body.dark-mode .project-link:hover {
color: #3498db;
}
body.dark-mode .btn-primary {
background-color: var(--dark-secondary);
color: white;
}
body.dark-mode .btn-primary:hover {
background-color: #3498db;
}
body.dark-mode .btn-secondary {
background-color: transparent;
color: var(--dark-text);
border: 2px solid var(--dark-text);
}
body.dark-mode .btn-secondary:hover {
background-color: var(--dark-text);
color: var(--dark-bg);
}
body.dark-mode .yalarba-section {
background: linear-gradient(135deg, var(--dark-card) 0%, #34495e 100%);
border-left: 5px solid var(--dark-secondary);
}
body.dark-mode .investment-cta {
background-color: var(--dark-border);
color: var(--dark-text);
}
body.dark-mode .timeline:before {
background: var(--dark-secondary);
}
body.dark-mode .timeline-item:before {
background: var(--dark-card);
border: 2px solid var(--dark-secondary);
}
body.dark-mode .highlight {
color: #f39c12;
}
/* Темная тема для социальных ссылок */
body.dark-mode .social_link {
background-color: var(--dark-card);
box-shadow: 0px 0px 14px 0px rgba(0, 0, 0, 0.3);
}
/* Темная тема для фото */
body.dark-mode .about-valitovgaziz-photo-box img {
box-shadow: 4px 4px 8px 9px rgba(0, 0, 0, 0.3);
filter: brightness(0.9);
}
/* ТЕМНАЯ ТЕМА ДЛЯ СЕКЦИИ "О РЕПОЗИТОРИИ" */
body.dark-mode .projects-grid {
background: transparent;
}
body.dark-mode .project-card {
background: var(--dark-card);
color: var(--dark-text);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3);
border-left: 4px solid var(--dark-secondary);
border: 1px solid var(--dark-border);
}
body.dark-mode .project-card h3 {
color: var(--dark-secondary);
}
body.dark-mode .current-info {
color: var(--dark-text);
border: 1px solid var(--dark-border);
}
body.dark-mode .current-info h3 {
color: var(--dark-secondary);
}
body.dark-mode .current-info ul {
color: var(--dark-text);
}
body.dark-mode .current-info strong {
color: var(--dark-text);
}
/* Темная тема для контактной секции */
body.dark-mode .contact-info p {
color: var(--dark-text);
}
body.dark-mode .contact-info a {
color: var(--dark-secondary);
}
body.dark-mode #saveContactBtn {
background: var(--dark-card);
color: var(--dark-secondary);
border: 2px solid var(--dark-secondary);
}
body.dark-mode #saveContactBtn:hover {
background: var(--dark-secondary);
color: var(--dark-text);
}
/* Темная тема для футера */
body.dark-mode .footer-box {
color: var(--dark-text);
}
body.dark-mode .footer-box a {
color: var(--dark-secondary);
}
body.dark-mode .footer-box ul {
color: var(--dark-text);
}
/* Темная тема для hero section */
body.dark-mode .hero {
background: linear-gradient(135deg, var(--dark-bg) 0%, #1a535c 100%);
color: var(--dark-text);
}
body.dark-mode .hero-description {
color: var(--dark-text);
}
body.dark-mode .hero-subtitle {
color: var(--dark-text);
}
/* Темная тема для секции "Обо мне" */
body.dark-mode .about {
background: var(--dark-card);
}
body.dark-mode .about-text {
color: var(--dark-text);
}
body.dark-mode .entrepreneur-highlights {
color: var(--dark-text);
}
body.dark-mode .highlight-item h4 {
color: var(--dark-secondary);
}
body.dark-mode .highlight-item p {
color: var(--dark-text);
}
@@ -1,226 +0,0 @@
/* Digital Background for Software Development Website */
/* Интеграция с существующей системой тем */
/* Используем переменные из darkTheme.css */
:root {
/* Light Theme Colors - интегрируем с существующими переменными */
--bg-primary-light: #f8f9fa;
--bg-secondary-light: #e9ecef;
--accent-primary-light: #007bff;
--accent-secondary-light: #6c757d;
--text-primary-light: #212529;
--particle-color-light: rgba(0, 123, 255, 0.1);
/* Dark Theme Colors - используем переменные из darkTheme.css */
--bg-primary-dark: var(--dark-bg, #1a252f);
--bg-secondary-dark: var(--dark-card, #2c3e50);
--accent-primary-dark: var(--dark-secondary, #2980b9);
--accent-secondary-dark: var(--dark-border, #34495e);
--text-primary-dark: var(--dark-text, #ecf0f1);
--particle-color-dark: rgba(41, 128, 185, 0.15);
/* Current Theme - defaults to light */
--bg-primary: var(--bg-primary-light);
--bg-secondary: var(--bg-secondary-light);
--accent-primary: var(--accent-primary-light);
--accent-secondary: var(--accent-secondary-light);
--text-primary: var(--text-primary-light);
--particle-color: var(--particle-color-light);
}
/* Интеграция с существующей темной темой */
body.dark-mode {
--bg-primary: var(--bg-primary-dark);
--bg-secondary: var(--bg-secondary-dark);
--accent-primary: var(--accent-primary-dark);
--accent-secondary: var(--accent-secondary-dark);
--text-primary: var(--text-primary-dark);
--particle-color: var(--particle-color-dark);
}
/* Base Body Styles */
body {
margin: 0;
padding: 0;
min-height: 100vh;
background: linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-secondary) 100%);
color: var(--text-primary);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
overflow-x: hidden;
position: relative;
}
/* Animated Background Elements */
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background:
radial-gradient(circle at 20% 80%, var(--particle-color) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, var(--particle-color) 0%, transparent 50%),
radial-gradient(circle at 40% 40%, var(--particle-color) 0%, transparent 50%);
animation: backgroundPulse 8s ease-in-out infinite;
z-index: -3;
}
/* Binary Code Rain Effect - ИСПРАВЛЕННЫЙ СТИЛЬ */
.binary-rain {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: -1;
opacity: 0.6;
}
.binary-digit {
position: absolute;
color: var(--accent-primary);
font-family: 'Courier New', monospace;
font-weight: bold;
animation: fall linear infinite;
text-shadow: 0 0 5px currentColor;
white-space: nowrap;
}
/* Circuit Board Grid */
.circuit-grid {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image:
linear-gradient(var(--accent-secondary) 1px, transparent 1px),
linear-gradient(90deg, var(--accent-secondary) 1px, transparent 1px);
background-size: 40px 40px;
opacity: 0.03;
z-index: -2;
animation: gridMove 20s linear infinite;
}
/* Floating Code Elements */
.floating-code {
position: fixed;
font-family: 'Courier New', monospace;
color: var(--accent-primary);
opacity: 0.1;
z-index: -1;
pointer-events: none;
}
.code-bracket { animation: float 15s ease-in-out infinite; }
.code-parenthesis { animation: float 18s ease-in-out infinite reverse; }
.code-brace { animation: float 20s ease-in-out infinite; }
.code-tag { animation: float 16s ease-in-out infinite reverse; }
/* Connection Nodes */
.connection-node {
position: fixed;
background: var(--accent-primary);
border-radius: 50%;
opacity: 0.2;
z-index: -1;
animation: nodePulse 4s ease-in-out infinite;
}
/* Data Flow Lines */
.data-flow {
position: fixed;
background: linear-gradient(90deg, transparent, var(--accent-primary), transparent);
opacity: 0.1;
z-index: -1;
animation: dataFlow 6s linear infinite;
}
/* ОБЯЗАТЕЛЬНО: Убедимся что основной контент поверх фона */
header, .section, footer {
position: relative;
z-index: 1;
}
/* Animations */
@keyframes backgroundPulse {
0%, 100% { opacity: 0.5; }
50% { opacity: 0.8; }
}
@keyframes fall {
to {
transform: translateY(100vh);
}
}
@keyframes float {
0%, 100% {
transform: translate(0, 0) rotate(0deg);
}
25% {
transform: translate(20px, 20px) rotate(5deg);
}
50% {
transform: translate(-15px, 30px) rotate(-5deg);
}
75% {
transform: translate(10px, -10px) rotate(3deg);
}
}
@keyframes gridMove {
0% { transform: translate(0, 0); }
100% { transform: translate(20px, 20px); }
}
@keyframes nodePulse {
0%, 100% { transform: scale(1); opacity: 0.2; }
50% { transform: scale(1.3); opacity: 0.4; }
}
@keyframes dataFlow {
0% { transform: translateX(-100%); opacity: 0; }
50% { opacity: 0.3; }
100% { transform: translateX(100%); opacity: 0; }
}
/* Interactive Elements */
.connection-node:hover {
opacity: 0.6;
transform: scale(1.5);
transition: all 0.3s ease;
}
/* Performance Optimizations */
@media (prefers-reduced-motion: reduce) {
.binary-digit,
.floating-code,
.connection-node,
.data-flow,
body::before {
animation: none;
}
}
/* Responsive Adjustments */
@media (max-width: 768px) {
.binary-digit {
font-size: 10px;
}
.circuit-grid {
background-size: 20px 20px;
}
.floating-code {
font-size: 8px;
}
}
/* Убедимся что кнопка переключения темы всегда поверх всего */
.theme-toggle {
z-index: 1000 !important;
}
@@ -1,78 +0,0 @@
footer {
text-align: center;
padding: 1em 0 0 0;
color: var(--dark);
font-size: 0.9rem;
border-radius: 1rem;
display: flex;
flex-direction: column;
}
.footer-links {
padding: 1em;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2rem;
justify-content: center;
}
.footer-section {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.footer-section h4 {
margin: 0 0 0.5rem 0;
font-weight: bold;
}
.two-column-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
border-left: 1px solid black;
}
.footer-box {
padding: 0.5rem;
}
.footer-box ul {
list-style: none;
padding: 0;
margin: 0;
text-align: left;
}
.footer-box li {
margin-bottom: 0.3rem;
line-height: 1.3;
}
.footer-box a {
color: inherit;
text-decoration: none;
}
.footer-box a:hover {
text-decoration: underline;
}
.footer-end-text {
margin: 2rem 0 3rem 0;
position: relative;
bottom: 0;
}
/* Адаптивность для мобильных устройств */
@media (max-width: 768px) {
.footer-links {
grid-template-columns: 1fr;
gap: 1.5rem;
}
.two-column-grid {
gap: 0.5rem;
}
}
@@ -1,112 +0,0 @@
/* Hero Section Styles */
.hero {
background: linear-gradient(135deg, var(--primary) 0%, #2fe892 100%);
padding: 4rem 2rem;
margin-bottom: 2rem;
border-radius: 10px;
}
.hero-content {
grid-column: 1 / -1;
display: grid;
grid-template-columns: 1fr;
gap: 1.5rem;
}
.hero-text {
display: flex;
flex-direction: column;
gap: 1rem;
}
.hero-subtitle {
font-size: 1.5rem;
margin-bottom: 1rem;
}
.highlight {
color: #137c5c; /* Яркий акцентный цвет */
}
.hero-description {
font-size: 1.1rem;
margin-bottom: 2rem;
line-height: 1.6;
}
.hero-buttons {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-top: 1rem;
}
.btn {
background-color: var(--secondary);
display: inline-block;
padding: 0.8rem 1.5rem;
border-radius: 5px;
text-decoration: none;
font-weight: bold;
transition: all 0.3s ease;
}
.btn-primary {
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.btn-secondary {
color: white;
border: 2px solid white;
}
.btn-secondary:hover {
background-color: #2980b9;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.hero-image {
flex: 0 0 300px;
text-align: center;
}
.hero-image img {
width: 100%;
max-width: 300px;
border-radius: 10px;
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
}
.resume-block {
justify-self: left;
}
#resume-link {
color: #2980b9;
/* Адаптивность для героя */
@media (max-width: 768px) {
.hero-content {
flex-direction: column;
text-align: center;
}
}
}
/* Адаптивность для героя */
@media (max-width: 768px) {
.hero-content {
flex-direction: column;
text-align: center;
}
.hero-buttons {
justify-content: center;
}
}
@@ -1,76 +0,0 @@
/* Добавьте в style.css */
a {
color: var(--secondary);
text-decoration: none;
transition: var(--transition);
font-weight: 500;
position: relative;
padding-right: 16px;
}
a:not(.btn):after {
content: "↗";
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
font-size: 0.8em;
opacity: 0;
transition: all 0.3s ease;
}
a:not(.btn):hover {
color: #2980b9;
padding-right: 20px;
}
a:not(.btn):hover:after {
opacity: 1;
right: -2px;
}
/* Для внутренних ссылок (без внешней иконки) */
a[href*="valitovgaziz.ru"]:after,
a[href*="#"]:after {
content: "→";
}
/* Для темной темы */
body.dark-mode a:not(.btn) {
color: var(--dark-secondary);
}
body.dark-mode a:not(.btn):hover {
color: #3498db;
}
/* Для ссылок в футере */
.footer-box a {
color: inherit;
transition: var(--transition);
border-bottom: 1px dotted transparent;
}
.footer-box a:hover {
border-bottom-color: currentColor;
}
/* Для ссылок в hero-секции */
.hero a {
color: #ffd166; /* Акцентный цвет из hero-секции */
font-weight: 600;
}
.hero a:hover {
color: #ffb347;
}
/* Для ссылок в карточках проектов */
.project-card a {
color: var(--dark-secondary);
font-weight: 600;
}
.project-card a:hover {
color: var(--secondary);
}
@@ -1,37 +0,0 @@
.current-info {
margin: 1rem;
padding: 1rem;
border-radius: 1rem;
}
.highlight-item:hover {
transform: translateY(-5px);
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.1);
}
/* Сетка для проектов */
.projects-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
margin: 2rem 0;
}
.project-card {
background: white;
padding: 1.5rem;
border-radius: var(--border-radius);
box-shadow: var(--shadow);
border-left: 4px solid var(--secondary);
transition: var(--transition);
display: grid;
grid-template-rows: auto 1fr;
gap: 1rem;
height: 80%;
}
.project-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.1);
}
@@ -1,18 +0,0 @@
#saveContactBtn {
padding: 10px 20px;
background: white;
color: #2541b2;
border: 2px solid #2541b2;
border-radius: 6px;
font-size: 15px;
transition: all 0.2s;
}
#saveContactBtn:hover {
background: #2541b2;
color: white;
}
#saveContactBtn.dark-mode {
background: --dark-card;
}
@@ -1,136 +0,0 @@
/* [file name]: skill_section.css */
.skills-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1.5rem;
margin-top: 1.5rem;
}
.skill-card {
background: linear-gradient(135deg, var(--secondary) 0%, #2980b9 100%);
color: white;
padding: 1.5rem;
border-radius: 12px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
transition: var(--transition);
display: grid;
grid-template-rows: auto auto 1fr auto;
gap: 0.8rem;
position: relative;
overflow: hidden;
}
.skill-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: rgba(255, 255, 255, 0.3);
}
.skill-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
}
.skill-header {
display: grid;
grid-template-columns: 1fr auto;
align-items: start;
gap: 0.5rem;
}
.skill-name {
font-size: 1.2rem;
font-weight: bold;
margin: 0;
color: white;
}
.skill-level {
background: rgba(255, 255, 255, 0.2);
padding: 0.3rem 0.8rem;
border-radius: 20px;
font-size: 0.8rem;
font-weight: bold;
backdrop-filter: blur(10px);
}
.skill-description {
font-size: 0.9rem;
line-height: 1.4;
opacity: 0.9;
margin: 0;
}
.skill-acquisition {
background: rgba(255, 255, 255, 0.1);
padding: 0.8rem;
border-radius: 8px;
font-size: 0.85rem;
line-height: 1.3;
}
.skill-growth {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.85rem;
color: #e8f4fc;
margin-top: 0.5rem;
}
.skill-growth::before {
content: '🚀';
font-size: 0.9rem;
}
/* Уровни навыков */
.skill-level.beginner { background: rgba(231, 76, 60, 0.8); }
.skill-level.intermediate { background: rgba(241, 196, 15, 0.8); }
.skill-level.advanced { background: rgba(46, 204, 113, 0.8); }
.skill-level.expert { background: rgba(52, 152, 219, 0.8); }
/* Темная тема */
body.dark-mode .skill-card {
background: linear-gradient(135deg, var(--dark-card) 0%, #34495e 100%);
border: 1px solid var(--dark-border);
}
body.dark-mode .skill-level {
background: rgba(255, 255, 255, 0.15);
}
body.dark-mode .skill-acquisition {
background: rgba(255, 255, 255, 0.08);
}
/* Адаптивность */
@media (max-width: 768px) {
.skills-container {
grid-template-columns: 1fr;
gap: 1rem;
}
.skill-card {
padding: 1.2rem;
}
.skill-header {
grid-template-columns: 1fr;
gap: 0.3rem;
}
.skill-level {
justify-self: start;
margin-top: 0.3rem;
}
}
@media (min-width: 1024px) {
.skills-container {
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
}
}
@@ -1,37 +0,0 @@
.social_links_block {
display: flex;
justify-content: space-around;
flex-direction: row;
flex-wrap: wrap;
width: 100%;
height: fit-content;
}
.social_link_block {
width: fit-content;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.social_links_block h4 {
width: fit-content;
}
.social_link {
width: 40px;
height: 40px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 50%;
-webkit-box-shadow: 0px 0px 14px 0px rgba(34, 60, 80, 0.2);
-moz-box-shadow: 0px 0px 14px 0px rgba(34, 60, 80, 0.2);
box-shadow: 0px 0px 14px 0px rgba(34, 60, 80, 0.2);
}
.social_link a {
width: fit-content;
height: auto;
}
@@ -1,53 +0,0 @@
/* Yalarba Investment Section */
.yalarba-section {
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
border-left: 5px solid var(--secondary);
}
.yalarba-header {
text-align: center;
margin-bottom: 2rem;
}
.yalarba-tagline {
font-size: 1.2rem;
color: var(--text);
font-style: italic;
}
.yalarba-stats {
display: flex;
justify-content: space-around;
margin: 0;
flex-wrap: wrap;
}
.stat {
text-align: center;
padding: 1rem;
}
.stat h3 {
font-size: 2.5rem;
color: var(--secondary);
margin: 0;
}
.yalarba-value ul {
list-style: none;
padding: 0;
}
.yalarba-value li {
padding: 0.5rem 0;
font-size: 1.1rem;
}
.investment-cta {
text-align: center;
margin-top: 2rem;
padding: 2rem;
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
+14
View File
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Valitov Gaziz | Технологический предприниматель</title>
<meta name="description" content="Валитов Газиз — технологический предприниматель и fullstack-разработчик. Создатель Yalarba.ru, EasySite102.ru." />
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
@@ -2,7 +2,7 @@
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
},
}
},
"exclude": ["node_modules", "dist"],
}
"exclude": ["node_modules", "dist"]
}
File diff suppressed because it is too large Load Diff
+19
View File
@@ -0,0 +1,19 @@
{
"name": "my_site",
"version": "0.0.1",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.5.22",
"vue-router": "^4.5.1"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.1",
"vite": "^7.1.7"
}
}
+52
View File
@@ -0,0 +1,52 @@
<template>
<div id="app" :class="{ 'menu-open': mobileMenuOpen }">
<TheHeader @toggle-theme="toggleTheme" @toggle-menu="toggleMenu" :theme="theme" :menu-open="mobileMenuOpen" />
<main class="main-content">
<router-view />
</main>
<TheFooter />
</div>
</template>
<script>
import TheHeader from './components/TheHeader.vue'
import TheFooter from './components/TheFooter.vue'
export default {
name: 'App',
components: {
TheHeader,
TheFooter,
},
data() {
return {
theme: 'dark',
mobileMenuOpen: false,
}
},
created() {
const saved = localStorage.getItem('theme')
if (saved) {
this.theme = saved
}
document.documentElement.setAttribute('data-theme', this.theme)
},
methods: {
toggleTheme() {
this.theme = this.theme === 'dark' ? 'light' : 'dark'
localStorage.setItem('theme', this.theme)
document.documentElement.setAttribute('data-theme', this.theme)
},
toggleMenu() {
this.mobileMenuOpen = !this.mobileMenuOpen
},
},
}
</script>
<style>
.main-content {
min-height: calc(100vh - 160px);
padding-top: 70px;
}
</style>
+174
View File
@@ -0,0 +1,174 @@
:root {
--bg: #ffffff;
--bg-secondary: #f5f7fa;
--text: #1a1a2e;
--text-secondary: #4a4a6a;
--accent: #2563eb;
--accent-hover: #1d4ed8;
--card-bg: #ffffff;
--border: #e2e8f0;
--header-bg: rgba(255, 255, 255, 0.9);
--skill-bg: #eef2ff;
--tag-bg: #e0e7ff;
--tag-text: #3730a3;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 30px rgba(0, 0, 0, 0.1);
--gradient-hero: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--max-width: 1100px;
--radius: 12px;
}
[data-theme="dark"] {
--bg: #0f172a;
--bg-secondary: #1e293b;
--text: #e2e8f0;
--text-secondary: #94a3b8;
--accent: #60a5fa;
--accent-hover: #3b82f6;
--card-bg: #1e293b;
--border: #334155;
--header-bg: rgba(15, 23, 42, 0.9);
--skill-bg: #1e293b;
--tag-bg: #1e293b;
--tag-text: #93c5fd;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
--shadow-lg: 0 10px 30px rgba(0, 0, 0, 0.4);
--gradient-hero: linear-gradient(135deg, #1e3a5f 0%, #2d1b69 100%);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background-color: var(--bg);
color: var(--text);
line-height: 1.6;
transition: background-color 0.3s, color 0.3s;
}
a {
color: var(--accent);
text-decoration: none;
transition: color 0.2s;
}
a:hover {
color: var(--accent-hover);
}
img {
max-width: 100%;
height: auto;
}
.container {
max-width: var(--max-width);
margin: 0 auto;
padding: 0 20px;
}
.section {
padding: 80px 0;
}
.section:nth-child(even) {
background-color: var(--bg-secondary);
}
.section-title {
font-size: 2rem;
font-weight: 700;
margin-bottom: 48px;
text-align: center;
position: relative;
}
.section-title::after {
content: '';
display: block;
width: 60px;
height: 4px;
background: var(--accent);
margin: 12px auto 0;
border-radius: 2px;
}
.grid {
display: grid;
gap: 24px;
}
.card {
background: var(--card-bg);
border-radius: var(--radius);
padding: 24px;
box-shadow: var(--shadow);
border: 1px solid var(--border);
transition: transform 0.2s, box-shadow 0.2s;
}
.card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-lg);
}
.btn {
display: inline-block;
padding: 12px 28px;
border-radius: 8px;
font-weight: 600;
font-size: 0.95rem;
cursor: pointer;
transition: all 0.2s;
border: none;
}
.btn-primary {
background: var(--accent);
color: #fff;
}
.btn-primary:hover {
background: var(--accent-hover);
color: #fff;
}
.btn-outline {
background: transparent;
border: 2px solid var(--accent);
color: var(--accent);
}
.btn-outline:hover {
background: var(--accent);
color: #fff;
}
.fade-in {
opacity: 0;
transform: translateY(20px);
transition: opacity 0.6s ease, transform 0.6s ease;
}
.fade-in.visible {
opacity: 1;
transform: translateY(0);
}
@media (max-width: 768px) {
.section {
padding: 48px 0;
}
.section-title {
font-size: 1.5rem;
margin-bottom: 32px;
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

@@ -0,0 +1,71 @@
<template>
<footer class="footer">
<div class="container footer-content">
<div class="footer-info">
<p>Уфа · Ufa · Өфө</p>
<p>&copy; 2026 Valitov Gaziz</p>
</div>
<div class="footer-links">
<a href="https://t.me/valitovgaziz" target="_blank" rel="noopener noreferrer" class="footer-link">
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z"/></svg>
</a>
<a href="https://vk.com/valitovgaziz" target="_blank" rel="noopener noreferrer" class="footer-link">
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M15.684 0H8.316C2.879 0 0 2.879 0 8.316v7.368C0 21.121 2.879 24 8.316 24h7.368C21.121 24 24 21.121 24 15.684V8.316C24 2.879 21.121 0 15.684 0zm3.6 16.535h-1.651c-.849 0-1.08-.595-1.728-1.289-.566-.604-1.05-1.097-1.89-1.097-.97 0-1.4.513-1.4 1.316v1.07c0 .456-.178.72-1.076.72-1.583 0-3.308-1.088-4.33-2.539-1.443-1.875-1.83-3.36-1.83-3.636 0-.177.151-.343.448-.343h1.652c.468 0 .64.227.82.735.633 1.937 1.699 3.633 2.345 3.633.22 0 .306-.11.306-.576v-2.22c-.111-1.048-.672-1.124-.672-1.504 0-.242.168-.44.45-.44h2.578c.347 0 .46.196.46.573v2.176c0 .346.149.44.254.44.224 0 .38-.094.598-.317.604-.7 1.118-1.847 1.118-1.847.088-.215.224-.423.513-.423h1.651c.493 0 .597.252.493.619-.307.953-1.577 2.764-1.577 2.764-.168.257-.224.381 0 .638.168.224.739.672 1.126 1.09.392.423.672.747.784.985.196.44-.056.735-.54.735z"/></svg>
</a>
<a href="mailto:valitovgaziz@yandex.ru" class="footer-link">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="M22 4L12 13L2 4"/></svg>
</a>
</div>
</div>
</footer>
</template>
<script>
export default {
name: 'TheFooter',
}
</script>
<style scoped>
.footer {
background: var(--bg-secondary);
border-top: 1px solid var(--border);
padding: 32px 0;
margin-top: 80px;
}
.footer-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.footer-info p {
color: var(--text-secondary);
font-size: 0.9rem;
}
.footer-links {
display: flex;
gap: 16px;
}
.footer-link {
color: var(--text-secondary);
transition: color 0.2s;
display: flex;
align-items: center;
}
.footer-link:hover {
color: var(--accent);
}
@media (max-width: 768px) {
.footer-content {
flex-direction: column;
gap: 16px;
text-align: center;
}
}
</style>
@@ -0,0 +1,178 @@
<template>
<header class="header">
<div class="container header-container">
<router-link to="/" class="logo">Valitov<span>Gaziz</span></router-link>
<nav class="nav" :class="{ 'nav-open': menuOpen }">
<router-link to="/" class="nav-link" @click="$emit('toggle-menu')">Главная</router-link>
<a href="#about" class="nav-link" @click="closeMenu">Обо мне</a>
<a href="#projects" class="nav-link" @click="closeMenu">Проекты</a>
<a href="#experience" class="nav-link" @click="closeMenu">Опыт</a>
<a href="#skills" class="nav-link" @click="closeMenu">Навыки</a>
<a href="#contact" class="nav-link" @click="closeMenu">Контакты</a>
<router-link to="/blog" class="nav-link" @click="$emit('toggle-menu')">Блог</router-link>
</nav>
<div class="header-actions">
<button class="theme-toggle" @click="$emit('toggle-theme')" :aria-label="theme === 'dark' ? 'Светлая тема' : 'Тёмная тема'">
<svg v-if="theme === 'dark'" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
<svg v-else width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
</button>
<button class="burger" @click="$emit('toggle-menu')" :aria-label="menuOpen ? 'Закрыть меню' : 'Открыть меню'">
<span></span><span></span><span></span>
</button>
</div>
</div>
</header>
</template>
<script>
export default {
name: 'TheHeader',
props: {
theme: { type: String, required: true },
menuOpen: { type: Boolean, default: false },
},
emits: ['toggle-theme', 'toggle-menu'],
methods: {
closeMenu() {
if (this.menuOpen) {
this.$emit('toggle-menu')
}
},
},
}
</script>
<style scoped>
.header {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
background: var(--header-bg);
backdrop-filter: blur(10px);
border-bottom: 1px solid var(--border);
transition: background 0.3s;
}
.header-container {
display: flex;
align-items: center;
justify-content: space-between;
height: 70px;
}
.logo {
font-size: 1.4rem;
font-weight: 700;
color: var(--text);
letter-spacing: -0.5px;
}
.logo span {
color: var(--accent);
}
.nav {
display: flex;
gap: 8px;
}
.nav-link {
color: var(--text-secondary);
font-size: 0.9rem;
font-weight: 500;
padding: 8px 16px;
border-radius: 8px;
transition: all 0.2s;
}
.nav-link:hover,
.router-link-exact-active {
color: var(--accent);
background: var(--skill-bg);
}
.header-actions {
display: flex;
align-items: center;
gap: 8px;
}
.theme-toggle {
background: none;
border: 1px solid var(--border);
color: var(--text);
cursor: pointer;
padding: 8px;
border-radius: 8px;
display: flex;
align-items: center;
transition: all 0.2s;
}
.theme-toggle:hover {
border-color: var(--accent);
color: var(--accent);
}
.burger {
display: none;
flex-direction: column;
gap: 5px;
background: none;
border: none;
cursor: pointer;
padding: 4px;
}
.burger span {
display: block;
width: 24px;
height: 2px;
background: var(--text);
border-radius: 2px;
transition: all 0.3s;
}
.menu-open .burger span:nth-child(1) {
transform: rotate(45deg) translate(5px, 5px);
}
.menu-open .burger span:nth-child(2) {
opacity: 0;
}
.menu-open .burger span:nth-child(3) {
transform: rotate(-45deg) translate(5px, -5px);
}
@media (max-width: 768px) {
.nav {
position: fixed;
top: 70px;
left: 0;
right: 0;
background: var(--bg);
flex-direction: column;
padding: 20px;
gap: 4px;
transform: translateY(-100%);
opacity: 0;
pointer-events: none;
transition: all 0.3s;
border-bottom: 1px solid var(--border);
z-index: 999;
}
.nav-open {
transform: translateY(0);
opacity: 1;
pointer-events: all;
}
.burger {
display: flex;
}
}
</style>
+11
View File
@@ -0,0 +1,11 @@
import './assets/main.css'
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(router)
app.mount('#app')
+20
View File
@@ -0,0 +1,20 @@
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'Home',
component: Home,
},
{
path: '/blog',
name: 'Blog',
component: () => import('../views/Blog.vue'),
},
],
})
export default router
+181
View File
@@ -0,0 +1,181 @@
<template>
<div class="blog-page">
<section class="blog-hero">
<div class="container">
<h1 class="blog-title">Блог</h1>
<p class="blog-subtitle">Мысли, идеи и заметки о разработке, технологиях и предпринимательстве</p>
</div>
</section>
<section class="section">
<div class="container">
<div class="blog-list">
<article v-for="(post, index) in posts" :key="index" class="blog-post card" :class="{ 'fade-in': true, 'visible': true }">
<div class="post-meta">
<span class="post-date">{{ post.date }}</span>
<span class="post-read-time">{{ post.readTime }}</span>
</div>
<h2 class="post-title">{{ post.title }}</h2>
<p class="post-excerpt">{{ post.excerpt }}</p>
<blockquote v-if="post.quote" class="post-quote">
{{ post.quote }}
</blockquote>
<p v-if="post.additional" class="post-excerpt">{{ post.additional }}</p>
<div class="post-tags">
<span v-for="(tag, tIndex) in post.tags" :key="tIndex" class="post-tag">{{ tag }}</span>
</div>
</article>
</div>
</div>
</section>
</div>
</template>
<script>
export default {
name: 'BlogView',
data() {
return {
posts: [
{
date: '20 марта 2024',
readTime: '6 мин чтения',
title: 'EasySite & YalArba: текущее состояние и планы развития',
excerpt: 'EasySite (B2B) — конструктор сайтов для отелей, санаториев, ресторанов. YalArba (B2C) — агрегатор для туристов с поиском, отзывами и системой бронирования.',
additional: 'Уже работают: JWT-авторизация, Docker-инфраструктура, SSL (Let\'s Encrypt), базовая аналитика. В бете: easysite102.ru и yalarba.ru.',
tags: ['EasySite', 'YalArba', 'Туризм', 'Стартап'],
},
{
date: '25 марта 2024',
readTime: '8 мин чтения',
title: 'Почему я создаю YalArba: история и миссия',
excerpt: 'История началась в 2017 году — работа на заводе и учёба не могли затмить желания создать что-то полезное. Не найдя бесплатных маршрутов для путешествий онлайн, решил сделать решение сам.',
additional: 'Большинство сервисов будут бесплатными — хочется предоставить доступные альтернативы для всех. Бизнес-модель строится на ценности от количества пользователей.',
quote: 'Технологии должны решать реальные проблемы людей, а не создавать новые.',
tags: ['История', 'Миссия', 'Социальный проект', 'Туризм'],
},
{
date: '15 марта 2024',
readTime: '5 мин чтения',
title: 'Новый этап развития Yalarba.ru',
excerpt: 'Завершён переход на новую архитектуру. Реализованы: обновлённый интерфейс поиска маршрутов, интеграция картографических сервисов, улучшенная система рекомендаций, подготовка мобильного приложения.',
tags: ['Yalarba', 'TravelTech', 'Разработка'],
},
{
date: '10 марта 2024',
readTime: '7 мин чтения',
title: 'Переход с Vue 2 на Vue 3: опыт и выводы',
excerpt: 'Ключевые преимущества: Composition API, улучшенная производительность, поддержка TypeScript, меньший размер бандла. Миграция прошла гладко, но потребовала внимания к деталям.',
tags: ['Vue3', 'Фронтенд', 'JavaScript'],
},
{
date: '5 марта 2024',
readTime: '4 мин чтения',
title: 'О важности сообщества в разработке',
excerpt: 'Профессиональный рост через сообщество: обратная связь, совместное обучение, поддержка и вдохновение. Нетворкинг и обмен опытом — ключ к развитию.',
tags: ['Сообщество', 'Разработка', 'IT'],
},
],
}
},
}
</script>
<style scoped>
.blog-hero {
padding: 80px 0 40px;
text-align: center;
background: var(--bg-secondary);
}
.blog-title {
font-size: 2.5rem;
font-weight: 800;
margin-bottom: 12px;
}
.blog-subtitle {
font-size: 1.1rem;
color: var(--text-secondary);
max-width: 600px;
margin: 0 auto;
}
.blog-list {
max-width: 700px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 32px;
}
.blog-post {
text-align: left;
}
.post-meta {
display: flex;
gap: 16px;
margin-bottom: 12px;
font-size: 0.85rem;
}
.post-date {
color: var(--accent);
font-weight: 600;
}
.post-read-time {
color: var(--text-secondary);
}
.post-title {
font-size: 1.4rem;
font-weight: 700;
margin-bottom: 12px;
line-height: 1.3;
}
.post-excerpt {
font-size: 0.95rem;
color: var(--text-secondary);
line-height: 1.7;
margin-bottom: 12px;
}
.post-quote {
border-left: 3px solid var(--accent);
padding: 12px 20px;
margin: 16px 0;
font-style: italic;
color: var(--text-secondary);
background: var(--bg-secondary);
border-radius: 0 8px 8px 0;
}
.post-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 16px;
}
.post-tag {
font-size: 0.8rem;
padding: 4px 12px;
border-radius: 6px;
background: var(--tag-bg);
color: var(--tag-text);
font-weight: 500;
}
@media (max-width: 768px) {
.blog-title {
font-size: 1.8rem;
}
.post-title {
font-size: 1.2rem;
}
}
</style>
+771
View File
@@ -0,0 +1,771 @@
<template>
<div class="home">
<section class="hero">
<div class="hero-bg"></div>
<div class="container hero-content">
<div class="hero-text">
<p class="hero-greeting">Привет, я</p>
<h1 class="hero-name">Валитов Газиз</h1>
<p class="hero-title">Технологический предприниматель &amp; Fullstack-разработчик</p>
<p class="hero-desc">
Создаю цифровые продукты, которые меняют жизнь людей к лучшему.
Основатель проектов Yalarba.ru и EasySite102.ru.
</p>
<div class="hero-buttons">
<a href="https://t.me/valitovgaziz" target="_blank" rel="noopener noreferrer" class="btn btn-primary">
Написать в Telegram
</a>
<router-link to="/blog" class="btn btn-outline">
Читать блог
</router-link>
</div>
<div class="hero-social">
<a href="https://t.me/valitovgaziz" target="_blank" rel="noopener noreferrer" class="social-link" title="Telegram">@valitovgaziz</a>
<a href="https://vk.com/valitovgaziz" target="_blank" rel="noopener noreferrer" class="social-link" title="VK">vk.com/valitovgaziz</a>
<a href="mailto:valitovgaziz@yandex.ru" class="social-link" title="Email">valitovgaziz@yandex.ru</a>
</div>
</div>
<div class="hero-image">
<div class="hero-photo">
<img src="/src/assets/photo.jpg" alt="Valitov Gaziz" class="photo-img" />
</div>
</div>
</div>
</section>
<section id="about" class="section">
<div class="container">
<h2 class="section-title">Обо мне</h2>
<div class="about-content">
<div class="about-text">
<p class="about-paragraph">
Родился в городе Кумертау в 1985 году. Окончил УГАТУ (Уфимский государственный авиационный технический университет), прошёл службу в армии, работал на производстве.
</p>
<p class="about-paragraph">
С 2015 года в IT прошёл путь от техника до основателя собственного технологического проекта. За плечами опыт работы с Go, Vue 3, Nuxt.js, PostgreSQL, Docker и другими современными технологиями.
</p>
<p class="about-paragraph">
Моя миссия создавать продукты, которые приносят реальную пользу людям и делают туризм в Башкортостане доступнее и удобнее.
</p>
</div>
<div class="about-highlights">
<div class="highlight-card">
<div class="highlight-icon">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>
</div>
<h4>Техническое видение</h4>
<p>Создаю масштабируемую архитектуру, выбираю правильные инструменты под задачи</p>
</div>
<div class="highlight-card">
<div class="highlight-icon">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>
</div>
<h4>Бизнес-ориентация</h4>
<p>Фокус на пользовательской ценности и устойчивых бизнес-моделях</p>
</div>
<div class="highlight-card">
<div class="highlight-icon">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
</div>
<h4>Практический подход</h4>
<p>От прототипа до продукта быстрое тестирование гипотез и итеративное развитие</p>
</div>
<div class="highlight-card">
<div class="highlight-icon">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>
</div>
<h4>Мотивация</h4>
<p>Создание проекта, который приносит пользу многим людям</p>
</div>
</div>
</div>
</div>
</section>
<section id="projects" class="section">
<div class="container">
<h2 class="section-title">Проекты</h2>
<div class="projects-grid">
<div class="project-card card">
<div class="project-header">
<h3 class="project-name">Yalarba.ru</h3>
<span class="project-status">Активный</span>
</div>
<p class="project-desc">
Туристическая платформа для Республики Башкортостан. Помогает путешественникам открывать новые места, строить маршруты и планировать поездки.
</p>
<div class="project-tech">
<span class="tech-tag">Go</span>
<span class="tech-tag">Nuxt.js 4</span>
<span class="tech-tag">PostgreSQL</span>
<span class="tech-tag">Docker</span>
</div>
</div>
<div class="project-card card">
<div class="project-header">
<h3 class="project-name">EasySite102.ru</h3>
<span class="project-status">Бета</span>
</div>
<p class="project-desc">
Конструктор сайтов для бизнеса в сфере туризма: отелей, санаториев, ресторанов. Часть экосистемы YalArba.
</p>
<div class="project-tech">
<span class="tech-tag">Vue 3</span>
<span class="tech-tag">Nuxt.js</span>
<span class="tech-tag">Go</span>
<span class="tech-tag">PostgreSQL</span>
</div>
</div>
<div class="project-card card">
<div class="project-header">
<h3 class="project-name">BegushiyBashkir.ru</h3>
<span class="project-status">Активный</span>
</div>
<p class="project-desc">
Беговой клуб. Сайт для бегового сообщества, основанного другом и партнёром Аминевым Загиром.
</p>
<div class="project-tech">
<span class="tech-tag">Vue 3</span>
<span class="tech-tag">Go</span>
<span class="tech-tag">PostgreSQL</span>
<span class="tech-tag">Docker</span>
</div>
</div>
</div>
</div>
</section>
<section id="experience" class="section">
<div class="container">
<h2 class="section-title">Опыт работы</h2>
<div class="timeline">
<div class="timeline-item">
<div class="timeline-dot"></div>
<div class="timeline-content card">
<div class="timeline-period">2020 настоящее время</div>
<h3 class="timeline-title">Основатель и Tech Lead</h3>
<p class="timeline-company">Yalarba.ru</p>
<ul class="timeline-duties">
<li>Микросервисы на Go + Nuxt.js 4</li>
<li>Проектирование и оптимизация PostgreSQL</li>
<li>Docker-инфраструктура, управление продуктом</li>
</ul>
</div>
</div>
<div class="timeline-item">
<div class="timeline-dot"></div>
<div class="timeline-content card">
<div class="timeline-period">2017 настоящее время</div>
<h3 class="timeline-title">Fullstack-разработчик (Контракты)</h3>
<p class="timeline-company">Фриланс / Проектная работа</p>
<ul class="timeline-duties">
<li>REST API на Go (GORM, Chi)</li>
<li>Фронтенд на Nuxt.js 4 / Vue 3</li>
<li>Посадочные страницы, интеграции</li>
</ul>
</div>
</div>
</div>
</div>
</section>
<section id="education" class="section">
<div class="container">
<h2 class="section-title">Образование</h2>
<div class="education-grid">
<div class="edu-card card">
<div class="edu-year">2025 н.в.</div>
<h4>МТИ Московский технологический институт</h4>
<p>Разработка программного обеспечения</p>
</div>
<div class="edu-card card">
<div class="edu-year">2021</div>
<h4>Университет Иннополис</h4>
<p>Java Enterprise Developer</p>
</div>
<div class="edu-card card">
<div class="edu-year">2016 2020</div>
<h4>Уфимский колледж статистики и информатики</h4>
<p>Техник по информационным системам</p>
</div>
<div class="edu-card card">
<div class="edu-year">2002 2005</div>
<h4>УГАТУ</h4>
<p>Технология сварочного производства</p>
</div>
</div>
</div>
</section>
<section id="skills" class="section">
<div class="container">
<h2 class="section-title">Навыки</h2>
<div class="skills-grid">
<div class="skill-card card">
<div class="skill-header">
<span class="skill-name">Golang</span>
<span class="skill-level">Продвинутый</span>
</div>
<div class="skill-bar"><div class="skill-fill" style="width: 90%"></div></div>
</div>
<div class="skill-card card">
<div class="skill-header">
<span class="skill-name">JavaScript</span>
<span class="skill-level">Продвинутый</span>
</div>
<div class="skill-bar"><div class="skill-fill" style="width: 85%"></div></div>
</div>
<div class="skill-card card">
<div class="skill-header">
<span class="skill-name">Vue 3</span>
<span class="skill-level">Средний</span>
</div>
<div class="skill-bar"><div class="skill-fill" style="width: 70%"></div></div>
</div>
<div class="skill-card card">
<div class="skill-header">
<span class="skill-name">Nuxt.js</span>
<span class="skill-level">Средний</span>
</div>
<div class="skill-bar"><div class="skill-fill" style="width: 65%"></div></div>
</div>
<div class="skill-card card">
<div class="skill-header">
<span class="skill-name">PostgreSQL</span>
<span class="skill-level">Средний</span>
</div>
<div class="skill-bar"><div class="skill-fill" style="width: 70%"></div></div>
</div>
<div class="skill-card card">
<div class="skill-header">
<span class="skill-name">Docker</span>
<span class="skill-level">Средний</span>
</div>
<div class="skill-bar"><div class="skill-fill" style="width: 70%"></div></div>
</div>
<div class="skill-card card">
<div class="skill-header">
<span class="skill-name">Java</span>
<span class="skill-level">Начальный</span>
</div>
<div class="skill-bar"><div class="skill-fill" style="width: 40%"></div></div>
</div>
<div class="skill-card card">
<div class="skill-header">
<span class="skill-name">Spring Framework</span>
<span class="skill-level">Начальный</span>
</div>
<div class="skill-bar"><div class="skill-fill" style="width: 35%"></div></div>
</div>
</div>
</div>
</section>
<section id="contact" class="section">
<div class="container">
<h2 class="section-title">Контакты</h2>
<div class="contact-content">
<p class="contact-text">Открыт к общению, сотрудничеству и новым проектам. Пишите!</p>
<div class="contact-links">
<a href="https://t.me/valitovgaziz" target="_blank" rel="noopener noreferrer" class="contact-item">
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z"/></svg>
<span>@valitovgaziz</span>
</a>
<a href="mailto:valitovgaziz@yandex.ru" class="contact-item">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="M22 4L12 13L2 4"/></svg>
<span>valitovgaziz@yandex.ru</span>
</a>
<a href="tel:+79625439343" class="contact-item">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/></svg>
<span>+7 (962) 543-93-43</span>
</a>
</div>
</div>
</div>
</section>
</div>
</template>
<script>
export default {
name: 'HomeView',
mounted() {
this.setupScrollObserver()
},
beforeUnmount() {
if (this.observer) {
this.observer.disconnect()
}
},
methods: {
setupScrollObserver() {
this.observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('visible')
}
})
},
{ threshold: 0.1 }
)
document.querySelectorAll('.fade-in').forEach((el) => {
this.observer.observe(el)
})
},
},
}
</script>
<style scoped>
.hero {
position: relative;
min-height: 90vh;
display: flex;
align-items: center;
overflow: hidden;
}
.hero-bg {
position: absolute;
inset: 0;
background: var(--gradient-hero);
opacity: 0.05;
z-index: 0;
}
.hero-content {
position: relative;
z-index: 1;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 60px;
align-items: center;
padding: 40px 0;
}
.hero-greeting {
font-size: 1.1rem;
color: var(--accent);
font-weight: 600;
margin-bottom: 8px;
}
.hero-name {
font-size: 3.5rem;
font-weight: 800;
line-height: 1.1;
margin-bottom: 16px;
letter-spacing: -1px;
}
.hero-title {
font-size: 1.2rem;
color: var(--text-secondary);
margin-bottom: 20px;
font-weight: 500;
}
.hero-desc {
font-size: 1rem;
color: var(--text-secondary);
line-height: 1.7;
margin-bottom: 32px;
}
.hero-buttons {
display: flex;
gap: 12px;
margin-bottom: 32px;
}
.hero-social {
display: flex;
gap: 24px;
flex-wrap: wrap;
}
.social-link {
font-size: 0.85rem;
color: var(--text-secondary);
font-weight: 500;
}
.social-link:hover {
color: var(--accent);
}
.hero-image {
display: flex;
justify-content: center;
}
.hero-photo {
width: 320px;
height: 320px;
border-radius: 50%;
overflow: hidden;
border: 4px solid var(--accent);
box-shadow: var(--shadow-lg);
display: flex;
align-items: center;
justify-content: center;
background: var(--bg-secondary);
}
.photo-img {
width: 100%;
height: 100%;
object-fit: cover;
}
.photo-placeholder {
color: var(--text-secondary);
opacity: 0.4;
display: flex;
align-items: center;
justify-content: center;
}
.about-content {
display: grid;
gap: 48px;
}
.about-paragraph {
font-size: 1.1rem;
line-height: 1.8;
color: var(--text-secondary);
margin-bottom: 16px;
}
.about-highlights {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
}
.highlight-card {
text-align: center;
padding: 32px 20px;
border-radius: var(--radius);
background: var(--card-bg);
border: 1px solid var(--border);
transition: transform 0.2s;
}
.highlight-card:hover {
transform: translateY(-4px);
}
.highlight-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 56px;
height: 56px;
border-radius: 16px;
background: var(--skill-bg);
color: var(--accent);
margin-bottom: 16px;
}
.highlight-card h4 {
font-size: 1rem;
margin-bottom: 8px;
}
.highlight-card p {
font-size: 0.85rem;
color: var(--text-secondary);
line-height: 1.5;
}
.projects-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 24px;
}
.project-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12px;
}
.project-name {
font-size: 1.2rem;
font-weight: 700;
}
.project-status {
font-size: 0.75rem;
padding: 4px 10px;
border-radius: 20px;
background: var(--tag-bg);
color: var(--tag-text);
font-weight: 600;
white-space: nowrap;
}
.project-desc {
font-size: 0.9rem;
color: var(--text-secondary);
line-height: 1.6;
margin-bottom: 16px;
}
.project-tech {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.tech-tag {
font-size: 0.8rem;
padding: 4px 12px;
border-radius: 6px;
background: var(--skill-bg);
color: var(--accent);
font-weight: 500;
}
.timeline {
position: relative;
max-width: 700px;
margin: 0 auto;
}
.timeline::before {
content: '';
position: absolute;
left: 20px;
top: 0;
bottom: 0;
width: 2px;
background: var(--border);
}
.timeline-item {
position: relative;
padding-left: 56px;
margin-bottom: 32px;
}
.timeline-dot {
position: absolute;
left: 12px;
top: 24px;
width: 18px;
height: 18px;
border-radius: 50%;
background: var(--accent);
border: 4px solid var(--bg);
z-index: 1;
}
.timeline-period {
font-size: 0.85rem;
color: var(--accent);
font-weight: 600;
margin-bottom: 4px;
}
.timeline-title {
font-size: 1.15rem;
font-weight: 700;
margin-bottom: 4px;
}
.timeline-company {
font-size: 0.9rem;
color: var(--text-secondary);
margin-bottom: 12px;
}
.timeline-duties {
list-style: none;
padding: 0;
}
.timeline-duties li {
position: relative;
padding-left: 20px;
margin-bottom: 6px;
font-size: 0.9rem;
color: var(--text-secondary);
}
.timeline-duties li::before {
content: '—';
position: absolute;
left: 0;
color: var(--accent);
}
.education-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px;
max-width: 700px;
margin: 0 auto;
}
.edu-card {
text-align: center;
}
.edu-year {
font-size: 0.85rem;
color: var(--accent);
font-weight: 600;
margin-bottom: 8px;
}
.edu-card h4 {
font-size: 0.95rem;
margin-bottom: 6px;
}
.edu-card p {
font-size: 0.85rem;
color: var(--text-secondary);
}
.skills-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
max-width: 700px;
margin: 0 auto;
}
.skill-header {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
}
.skill-name {
font-weight: 600;
font-size: 0.95rem;
}
.skill-level {
font-size: 0.8rem;
color: var(--text-secondary);
}
.skill-bar {
height: 8px;
background: var(--bg-secondary);
border-radius: 4px;
overflow: hidden;
}
.skill-fill {
height: 100%;
background: var(--gradient-hero);
border-radius: 4px;
transition: width 1s ease;
}
.contact-content {
text-align: center;
max-width: 500px;
margin: 0 auto;
}
.contact-text {
font-size: 1.1rem;
color: var(--text-secondary);
margin-bottom: 32px;
}
.contact-links {
display: flex;
flex-direction: column;
gap: 16px;
}
.contact-item {
display: flex;
align-items: center;
gap: 12px;
padding: 16px 24px;
border-radius: var(--radius);
background: var(--card-bg);
border: 1px solid var(--border);
color: var(--text);
font-size: 1rem;
transition: all 0.2s;
}
.contact-item:hover {
border-color: var(--accent);
color: var(--accent);
transform: translateX(4px);
}
.contact-item svg {
flex-shrink: 0;
}
@media (max-width: 768px) {
.hero-content {
grid-template-columns: 1fr;
text-align: center;
gap: 32px;
}
.hero-name {
font-size: 2.2rem;
}
.hero-buttons {
justify-content: center;
}
.hero-social {
justify-content: center;
}
.hero-photo {
width: 200px;
height: 200px;
}
.about-highlights {
grid-template-columns: repeat(2, 1fr);
}
.projects-grid {
grid-template-columns: 1fr;
}
.education-grid {
grid-template-columns: 1fr;
}
.skills-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 480px) {
.about-highlights {
grid-template-columns: 1fr;
}
.hero-name {
font-size: 1.8rem;
}
.hero-buttons {
flex-direction: column;
}
}
</style>
@@ -2,13 +2,10 @@ import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
vueDevTools(),
],
resolve: {
alias: {
@@ -16,9 +13,7 @@ export default defineConfig({
},
},
server: {
fs: {
strict: false,
},
},
clearScreen: true,
host: true,
port: 3002
}
})
-12
View File
@@ -1,12 +0,0 @@
# DB environment variabels
DB_HOST=localhost
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=postgres
DB_NAME=mydb
APP_PORT=8080
JWT_SECRET=secret
UPLOAD_PATH=./storage/uploads
ENVIRONMENT=development
LOG_LEVEL=debug
API_ES_APP_PORT=8088
-20
View File
@@ -1,20 +0,0 @@
FROM golang:1.25.1-alpine
WORKDIR /app
# Устанавливаем зависимости для компиляции
RUN apk add --no-cache gcc musl-dev
# Копируем go.mod и go.sum
COPY go.mod go.sum ./
RUN go mod download
# Копируем исходный код
COPY . .
# Компилируем БЕЗ CGO (указываем путь к main.go)
RUN CGO_ENABLED=0 GOOS=linux go build -o bin/main ./cmd/main.go
EXPOSE 8081
CMD ["./bin/main"]
-71
View File
@@ -1,71 +0,0 @@
package main
import (
"log"
"net/http"
"api_es/internal/config"
"api_es/internal/database"
"api_es/internal/router"
"api_es/pkg/logger"
"go.uber.org/zap"
)
func main() {
// Загрузка конфигурации приложения из файлов окружения или конфигурационных файлов
// Конфигурация включает параметры БД, уровень логирования, порт приложения и т.д.
cfg := config.Load()
// Инициализация логгера с указанным уровнем логирования и окружением (dev/prod)
// Логгер будет настроен соответствующим образом для заданного окружения
logger.Init(cfg.LogLevel, cfg.Environment)
// Получение инстанса логгера для использования во всем приложении
zapLogger := logger.Get()
// Логирование старта приложения с указанием используемого стека технологий
zapLogger.Info("Start api_es REST API on stack Golang (gorm, chi) and PostgresDB connect")
// Инициализация подключения к базе данных PostgreSQL с использованием параметров из конфигурации
// Возвращается объект gorm.DB для работы с ORM
db, err := database.NewPostgresConnection(cfg)
if err != nil {
// Критическая ошибка подключения к БД - приложение не может работать без БД
zapLogger.Panic("Failed to connect to database:", zap.Error(err))
}
// Получение низкоуровневого объекта *sql.DB из gorm.DB для выполнения операций,
// не поддерживаемых напрямую gorm (например, Ping)
sqlDB, err := db.DB()
if err != nil {
// Ошибка получения инстанса БД, но приложение может продолжить работу
zapLogger.Error("failed to get database instance", zap.Error(err))
}
// Проверка доступности базы данных через ping-запрос
// Убеждаемся, что соединение активно и БД отвечает
if err := sqlDB.Ping(); err != nil {
zapLogger.Error("database ping failed", zap.Error(err))
}
// Успешная проверка соединения с БД
zapLogger.Info("database ping successful")
// Настройка маршрутизатора (роутера) для обработки HTTP-запросов
// Передаем подключение к БД и конфигурацию для инициализации обработчиков
zapLogger.Info("setup router")
r := router.SetupRouter(db, cfg)
// Запуск HTTP-сервера на порту, указанном в конфигурации
// Сервер начинает прослушивать входящие соединения
zapLogger.Info("Server starting on port %s", zap.String("AppPort", cfg.AppPort))
log.Printf("Server starting on port %s", cfg.AppPort)
// Запуск HTTP-сервера с указанным роутером
// ListenAndServe блокирует выполнение и обрабатывает входящие запросы
// В случае ошибки запуска сервера, логируем ошибку и завершаем приложение
if err := http.ListenAndServe(":"+cfg.AppPort, r); err != nil {
log.Fatal("Failed to start server:", err)
}
}

Some files were not shown because too many files have changed in this diff Show More