From 95c7b64d4c8657ccc555bd11ca2eddee8bed5843 Mon Sep 17 00:00:00 2001 From: Web-serfer Date: Wed, 29 Apr 2026 20:23:07 +0500 Subject: [PATCH] =?UTF-8?q?=D0=A1=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD=20=D0=BA?= =?UTF-8?q?=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5=D1=82=20=D1=81=D1=87=D0=B5?= =?UTF-8?q?=D1=82=D1=87=D0=B8=D0=BA=D0=B0=20=D1=81=D0=B0=D0=B9=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 5 +- .../1777466303_created_site_visitors.js | 103 ++++++++++++++++ .../1777466913_updated_site_visitors.js | 28 +++++ .../1777466930_updated_site_visitors.js | 20 ++++ .../1777468200_updated_site_visitors.js | 20 ++++ .../1777472256_updated_site_visitors.js | 22 ++++ .../src/components/layout/footer/Footer.astro | 66 ++++++----- .../layout/footer/VisitorCounter.astro | 69 +++++++++++ frontend/src/icons/users-group.svg | 6 + frontend/src/icons/users-total.svg | 6 + frontend/src/pages/api/visitors.ts | 111 ++++++++++++++++++ 11 files changed, 425 insertions(+), 31 deletions(-) create mode 100644 backend/pb_migrations/1777466303_created_site_visitors.js create mode 100644 backend/pb_migrations/1777466913_updated_site_visitors.js create mode 100644 backend/pb_migrations/1777466930_updated_site_visitors.js create mode 100644 backend/pb_migrations/1777468200_updated_site_visitors.js create mode 100644 backend/pb_migrations/1777472256_updated_site_visitors.js create mode 100644 frontend/src/components/layout/footer/VisitorCounter.astro create mode 100644 frontend/src/icons/users-group.svg create mode 100644 frontend/src/icons/users-total.svg create mode 100644 frontend/src/pages/api/visitors.ts diff --git a/AGENTS.md b/AGENTS.md index 8e4a1f4..037bea7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,6 +7,7 @@ - Все изменения должны быть предварительно объяснены пользователю - Перед решением конкретной задачи всегда составлять план - После внесения изменений в код - проводить проверку - только после этого приступать к дальнейшему решению задачи + - **НЕ производить сборку проекта (`bun run build`) без явного разрешения пользователя** 2. **Прозрачность действий** - Ассистент должен объяснить, какие изменения планируется внести @@ -36,14 +37,14 @@ 8 **Проверка типов данных** - Проверять проект на ошибки типизации через команду `bun run tsc --noEmit -p frontend/tsconfig.json` - - Не производить сборку проекта без моего разрешения + - НЕ производить сборку проекта (`bun run build`) без явного разрешения пользователя - В проекте не должно быть типов any - Все интерфейсы компонентов прописывать в файле globalInterfaces.ts - При работе с PocketBase использовать актуальные сигнатуры методов из файла `D:\Verstka\production\astro_minivan\frontend\node_modules\pocketbase\dist\pocketbase.es.d.ts` 9 **Плагин @astrojs/sitemap** - Обязательно к установке в проект пакета @astrojs/sitemap - - Обязательно к созданию в проекте файла .nvmrc + - Обязательно к созданию в проекте файл .nvmrc ## Технические правила (Astro) diff --git a/backend/pb_migrations/1777466303_created_site_visitors.js b/backend/pb_migrations/1777466303_created_site_visitors.js new file mode 100644 index 0000000..3e5cd05 --- /dev/null +++ b/backend/pb_migrations/1777466303_created_site_visitors.js @@ -0,0 +1,103 @@ +/// +migrate((app) => { + const collection = new Collection({ + "createRule": null, + "deleteRule": null, + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "help": "", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "help": "", + "hidden": false, + "id": "text791980464", + "max": 0, + "min": 0, + "name": "visitor_hash", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "help": "", + "hidden": false, + "id": "text2783163181", + "max": 0, + "min": 0, + "name": "ip", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "help": "", + "hidden": false, + "id": "text3293145029", + "max": 0, + "min": 0, + "name": "user_agent", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "id": "pbc_2651661972", + "indexes": [], + "listRule": null, + "name": "site_visitors", + "system": false, + "type": "base", + "updateRule": null, + "viewRule": null + }); + + return app.save(collection); +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_2651661972"); + + return app.delete(collection); +}) diff --git a/backend/pb_migrations/1777466913_updated_site_visitors.js b/backend/pb_migrations/1777466913_updated_site_visitors.js new file mode 100644 index 0000000..842c27a --- /dev/null +++ b/backend/pb_migrations/1777466913_updated_site_visitors.js @@ -0,0 +1,28 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("pbc_2651661972") + + // update collection data + unmarshal({ + "createRule": "", + "deleteRule": "@request.auth.id != \"\"", + "listRule": "@request.auth.id != \"\"", + "updateRule": "@request.auth.id != \"\"", + "viewRule": "@request.auth.id != \"\" " + }, collection) + + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_2651661972") + + // update collection data + unmarshal({ + "createRule": null, + "deleteRule": null, + "listRule": null, + "updateRule": null, + "viewRule": null + }, collection) + + return app.save(collection) +}) diff --git a/backend/pb_migrations/1777466930_updated_site_visitors.js b/backend/pb_migrations/1777466930_updated_site_visitors.js new file mode 100644 index 0000000..e650eff --- /dev/null +++ b/backend/pb_migrations/1777466930_updated_site_visitors.js @@ -0,0 +1,20 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("pbc_2651661972") + + // update collection data + unmarshal({ + "createRule": "@request.method = \"POST\"" + }, collection) + + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_2651661972") + + // update collection data + unmarshal({ + "createRule": "" + }, collection) + + return app.save(collection) +}) diff --git a/backend/pb_migrations/1777468200_updated_site_visitors.js b/backend/pb_migrations/1777468200_updated_site_visitors.js new file mode 100644 index 0000000..d36f60c --- /dev/null +++ b/backend/pb_migrations/1777468200_updated_site_visitors.js @@ -0,0 +1,20 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("pbc_2651661972") + + // update collection data + unmarshal({ + "createRule": "" + }, collection) + + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_2651661972") + + // update collection data + unmarshal({ + "createRule": "@request.method = \"POST\"" + }, collection) + + return app.save(collection) +}) diff --git a/backend/pb_migrations/1777472256_updated_site_visitors.js b/backend/pb_migrations/1777472256_updated_site_visitors.js new file mode 100644 index 0000000..735b763 --- /dev/null +++ b/backend/pb_migrations/1777472256_updated_site_visitors.js @@ -0,0 +1,22 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("pbc_2651661972") + + // update collection data + unmarshal({ + "listRule": "", + "viewRule": "" + }, collection) + + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_2651661972") + + // update collection data + unmarshal({ + "listRule": "@request.auth.id != \"\"", + "viewRule": "@request.auth.id != \"\" " + }, collection) + + return app.save(collection) +}) diff --git a/frontend/src/components/layout/footer/Footer.astro b/frontend/src/components/layout/footer/Footer.astro index a299cbd..bb6562d 100644 --- a/frontend/src/components/layout/footer/Footer.astro +++ b/frontend/src/components/layout/footer/Footer.astro @@ -1,5 +1,6 @@ --- import { COMPANY } from '@constants'; +import VisitorCounter from './VisitorCounter.astro'; const sectionsLinks = [ { label: 'Услуги', href: '/services' }, @@ -25,10 +26,8 @@ const currentYear = new Date().getFullYear().toString();
\ No newline at end of file diff --git a/frontend/src/components/layout/footer/VisitorCounter.astro b/frontend/src/components/layout/footer/VisitorCounter.astro new file mode 100644 index 0000000..f1476fa --- /dev/null +++ b/frontend/src/components/layout/footer/VisitorCounter.astro @@ -0,0 +1,69 @@ +--- +import { Icon } from 'astro-icon/components'; + +interface Props { + today?: number; + total?: number; +} + +const { today = 0, total = 0 } = Astro.props; +--- + +
+
+ + {today} сегодня +
+
+ + {total} всего +
+
+ + + + \ No newline at end of file diff --git a/frontend/src/icons/users-group.svg b/frontend/src/icons/users-group.svg new file mode 100644 index 0000000..a7a2ee4 --- /dev/null +++ b/frontend/src/icons/users-group.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/frontend/src/icons/users-total.svg b/frontend/src/icons/users-total.svg new file mode 100644 index 0000000..19320c8 --- /dev/null +++ b/frontend/src/icons/users-total.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/frontend/src/pages/api/visitors.ts b/frontend/src/pages/api/visitors.ts new file mode 100644 index 0000000..9bfdc1e --- /dev/null +++ b/frontend/src/pages/api/visitors.ts @@ -0,0 +1,111 @@ +import type { APIRoute } from 'astro'; +import crypto from 'crypto'; + +const POCKETBASE_URL = import.meta.env.POCKETBASE_URL || 'http://127.0.0.1:8090'; + +function getClientIp(request: Request): string { + const forwarded = request.headers.get('x-forwarded-for'); + if (forwarded) { + return forwarded.split(',')[0].trim(); + } + const realIp = request.headers.get('x-real-ip'); + if (realIp) { + return realIp; + } + const host = request.headers.get('host') || 'localhost'; + return host.includes('localhost') || host.includes('127.0.0.1') ? 'localhost' : 'unknown'; +} + +function generateVisitorHash(ip: string, userAgent: string): string { + return crypto.createHash('sha256').update(ip + userAgent).digest('hex').slice(0, 32); +} + +async function pbRequest(method: string, path: string, body?: object) { + const url = `${POCKETBASE_URL}${path}`; + const options: RequestInit = { + method, + headers: { + 'Content-Type': 'application/json', + 'Origin': 'http://127.0.0.1:4321', + }, + }; + if (body) options.body = JSON.stringify(body); + + const res = await fetch(url, options); + if (!res.ok) { + const err = await res.text(); + throw new Error(`PB ${method} ${path}: ${res.status} - ${err}`); + } + return res.json(); +} + +function jsonResponse(data: object, status = 200): Response { + return new Response(JSON.stringify(data), { + status, + headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' } + }); +} + +export const GET: APIRoute = async ({ request }) => { + try { + const ip = getClientIp(request); + const userAgent = request.headers.get('user-agent') || 'unknown'; + const visitorHash = generateVisitorHash(ip, userAgent); + + const now = new Date(); + const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000); + const yesterdayStr = yesterday.toISOString().split('T')[0] + ' 00:00:00.000Z'; + + // Проверяем был ли посетитель за последние 24 часа + const filterLast24h = `visitor_hash="${visitorHash}" && created >= "${yesterdayStr}"`; + + let existingVisitor; + try { + const res = await pbRequest('GET', `/api/collections/site_visitors/records?filter=${encodeURIComponent(filterLast24h)}&perPage=1`); + existingVisitor = { totalItems: res.totalItems || 0 }; + } catch { + existingVisitor = { totalItems: 0 }; + } + + let isNewVisitor = false; + + // Создаём запись только если НЕ было посещения за последние 24ч + if (existingVisitor.totalItems === 0) { + isNewVisitor = true; + try { + await pbRequest('POST', '/api/collections/site_visitors/records', { + visitor_hash: visitorHash, + ip: ip, + user_agent: userAgent, + }); + } catch { + // ignore + } + } + + // Получаем статистику за сегодня + const filterToday = `created >= "${yesterdayStr}"`; + let todayCount = 0; + let totalCount = 0; + + try { + const resToday = await pbRequest('GET', `/api/collections/site_visitors/records?filter=${encodeURIComponent(filterToday)}&perPage=1`); + todayCount = resToday.totalItems || 0; + } catch { + todayCount = 0; + } + + try { + const resTotal = await pbRequest('GET', '/api/collections/site_visitors/records?perPage=1'); + totalCount = resTotal.totalItems || 0; + } catch { + totalCount = 0; + } + + return jsonResponse({ todayVisitors: todayCount, totalVisitors: totalCount, isNewVisitor }, 200); + + } catch (error) { + console.error('[Visitors] Error:', error); + return jsonResponse({ error: 'Внутренняя ошибка сервера' }, 500); + } +}; \ No newline at end of file