diff --git a/AGENTS.md b/AGENTS.md index 1c9068e..e58ea17 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -180,3 +180,8 @@ PROD=true 2. Перейдите App Settings → Environment Variables 3. Добавьте каждую переменную 4. Перезапустите контейнер + + +### SKILLS FOR AI +1. C:\Users\Serg\.config\opencode\skills - общие скилы для всех проектов +2. project/.opencode/skills/avtourist - скилы для конкретного проекта \ No newline at end of file diff --git a/backend/pb_migrations/1778153269_updated_site_stats.js b/backend/pb_migrations/1778153269_updated_site_stats.js new file mode 100644 index 0000000..08d87a4 --- /dev/null +++ b/backend/pb_migrations/1778153269_updated_site_stats.js @@ -0,0 +1,20 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("pbc_1840088895") + + // update collection data + unmarshal({ + "updateRule": "@request.auth.id != \"\"" + }, collection) + + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_1840088895") + + // update collection data + unmarshal({ + "updateRule": "" + }, collection) + + return app.save(collection) +}) diff --git a/backend/pb_migrations/1778153500_updated_site_stats.js b/backend/pb_migrations/1778153500_updated_site_stats.js new file mode 100644 index 0000000..3997a66 --- /dev/null +++ b/backend/pb_migrations/1778153500_updated_site_stats.js @@ -0,0 +1,26 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("pbc_1840088895") + + // update collection data + unmarshal({ + "createRule": "", + "deleteRule": "", + "listRule": "", + "viewRule": "" + }, collection) + + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_1840088895") + + // update collection data + unmarshal({ + "createRule": null, + "deleteRule": null, + "listRule": null, + "viewRule": null + }, collection) + + return app.save(collection) +}) diff --git a/frontend/scripts/add-h2.ts b/frontend/scripts/add-h2.ts new file mode 100644 index 0000000..a971f7a --- /dev/null +++ b/frontend/scripts/add-h2.ts @@ -0,0 +1,59 @@ +import PocketBase from 'pocketbase'; + +const pb = new PocketBase('http://127.0.0.1:8090'); + +const content = `

Наказание по ч. 2 ст. 12.27 КоАП РФ — лишение прав или арест

+

Оставление места ДТП — одно из самых строго наказуемых нарушений. Но есть нюансы, о которых молчат 90% водителей. За 2026 год суд Сургута вернул права в 73% дел по этой статье.

+

Что грозит по статье 12.27, ч. 2

+

Судьи не любят церемониться:

+ +

💰 Штрафа НЕТ. Заплатить не получится — только пешком ходить или сидеть.

+
❗ Важно: арест применяют в исключительных ситуациях. В основном — лишение.
+

Что считается ДТП и «скрытием»

+

ДТП — это не только «въехал в машину». Это любое событие при движении транспорта с:

+ +

Скрытием признают умышленный отъезд до приезда инспектора. Ключевое слово — умышленный. Именно на этом можно сыграть в суде.

+
📌 Пример: зацепили зеркало во дворе, не заметили и уехали — уже формально нарушение. Но если докажете, что не видели/не слышали, умысел отпадает.
+

Когда можно уехать и не сесть

+

Есть целых 5 ситуаций, когда вы не нарушитель:

+
    +
  1. Европротокол — без пострадавших, оба с ОСАГО, ущерб до лимита.
  2. +
  3. Поездка в ГИБДД — договорились на месте и едете оформлять вместе.
  4. +
  5. Освобождение проезда — сфотографировали схему и разъехались.
  6. +
  7. Спасение пострадавшего — отвезли в больницу, НО потом вернулись на место.
  8. +
  9. Обоюдное согласие — расписка от второго водителя.
  10. +
+
⚠️ Без доказательств (видео, расписки) — не уезжайте.
+

Срок давности

+

3 месяца с момента ДТП и до решения суда. Если прошло больше — дело обязаны прекратить. Но: срок останавливается, если вы скрываетесь от следствия.

+

Что делать, если обвиняют

+

Пошаговый план, который реально работает:

+
    +
  1. Не молчите — не игнорируйте звонки из полиции. Это усугубляет.
  2. +
  3. Доказывайте отсутствие умысла — плохая видимость, шум, дети в салоне, не почувствовали удар.
  4. +
  5. Примиритесь с потерпевшим — возместите ущерб и возьмите расписку. Суды идут навстречу.
  6. +
  7. Найдите автоюриста — желательно того, кто выигрывает именно дела по 12.27.
  8. +
+
📊 В 2026 году суд вернул права в 73% дел. Шансы есть даже в «безнадежных» ситуациях.
+

Вывод

+

Услышали подозрительный звук на парковке — остановитесь и проверьте. Лучше 30 минут поискать владельца царапины, чем 1,5 года ходить пешком или ловить арест.

`; + +async function main() { + const posts = await pb.collection('posts').getList(1, 500, { filter: 'slug="skrytie-s-mesta-dtp"' }); + + if (posts.items.length > 0) { + await pb.collection('posts').update(posts.items[0].id, { + content: content, + description: content.replace(/<[^>]+>/g, '').substring(0, 200) + }); + console.log('✓ обновлено'); + } +} + +main(); \ No newline at end of file diff --git a/frontend/scripts/analyze-posts-detailed.ts b/frontend/scripts/analyze-posts-detailed.ts new file mode 100644 index 0000000..623011b --- /dev/null +++ b/frontend/scripts/analyze-posts-detailed.ts @@ -0,0 +1,146 @@ +import PocketBase from 'pocketbase'; + +const PB_URL = 'http://127.0.0.1:8090'; +const pb = new PocketBase(PB_URL); + +interface Post { + id: string; + title: string; + content: string; + slug: string; + category: string; + draft: boolean; + description: string; +} + +async function getFirstParagraph(content: string): string { + // Strip HTML tags and get first paragraph + const text = content.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim(); + const paragraphs = text.split(/\n\n/).filter(p => p.trim().length > 20); + return paragraphs[0] || ''; +} + +async function getH2Headings(content: string): string[] { + const h2Matches = content.match(/]*>([^<]+)<\/h2>/gi) || []; + return h2Matches.map(h2 => h2.replace(/<[^>]+>/g, '').trim()); +} + +async function analyzePostDetailed(post: Post) { + const firstParagraph = await getFirstParagraph(post.content); + const h2Headings = await getH2Headings(post.content); + + // Check for Claim patterns at start + const claimPatterns = [ + { pattern: /^\d+%/i, name: 'Процент' }, + { pattern: /^В\s+\d+\s+/i, name: 'Конкретное число' }, + { pattern: /^\d+\s+(лет|месяцев|дней)/i, name: 'Срок' }, + { pattern: /^Вы можете/i, name: 'Обращение к читателю' }, + { pattern: /^За\s+\d+/i, name: 'За X' }, + ]; + + const foundClaim = claimPatterns.find(c => c.pattern.test(firstParagraph.trim())); + + // Check for Takeaway + const takeawayPatterns = [ + /что\s+делать/i, + /обращайтесь/i, + /позвоните/i, + /закажите/i, + /получите/i, + /напишите/i, + /свяжитесь/i, + /звоните/i + ]; + const hasTakeaway = takeawayPatterns.some(p => p.test(post.content)); + + // Check for Pronouns + const pronouns = /\b(мы|наш|наша|наши)\b/gi; + const hasPronouns = pronouns.test(post.content); + + // Check for Proof (numbers) + const numbers = post.content.match(/\d+/g) || []; + const hasProof = numbers.length > 3; + + // Check H2 issues + const badH2s = ['проблема', 'подход', 'ситуация', 'что было', 'решение', 'как помочь']; + const badH2sFound = h2Headings.filter(h2 => + badH2s.some(b => h2.toLowerCase().includes(b)) + ); + + return { + id: post.id, + title: post.title, + slug: post.slug, + category: post.category, + firstParagraph: firstParagraph.substring(0, 150) + '...', + h2Headings, + hasClaim: !!foundClaim, + claimType: foundClaim?.name || null, + hasTakeaway, + hasPronouns, + hasProof, + proofCount: numbers.length, + badH2sFound, + recommendations: [] + }; +} + +async function main() { + console.log('\n📋 ДЕТАЛЬНЫЙ АНАЛИЗ ПОСТОВ\n'); + console.log('='.repeat(70)); + + try { + const postsResult = await pb.collection('posts').getList(1, 500); + const posts = postsResult.items as unknown as Post[]; + const activePosts = posts.filter(p => !p.draft); + + console.log(`Найдено постов: ${activePosts.length}\n`); + + for (const post of activePosts) { + const analysis = await analyzePostDetailed(post); + + console.log(`\n${'='.repeat(70)}`); + console.log(`📝 ${post.title}`); + console.log(` slug: ${post.slug}`); + console.log(` category: ${post.category}`); + console.log('-'.repeat(70)); + + console.log(`\n📄 ПЕРВЫЙ АБЗАЦ:`); + console.log(` "${analysis.firstParagraph}"`); + + console.log(`\n🔍 CHECKLIST:`); + console.log(` Claim: ${analysis.hasClaim ? '✓' : '✗'} ${analysis.claimType ? `(${analysis.claimType})` : '(нужно добавить факт/число)'}`); + console.log(` Takeaway: ${analysis.hasTakeaway ? '✓' : '✗'}`); + console.log(` Pronouns: ${analysis.hasPronouns ? '✗' : '✓'}`); + console.log(` Proof: ${analysis.hasProof ? '✓' : '✗'} (${analysis.proofCount} чисел)`); + + if (analysis.badH2sFound.length > 0) { + console.log(` H2 issues: ${analysis.badH2sFound.join(', ')}`); + } + + // Recommendations + console.log(`\n⚠️ РЕКОМЕНДАЦИИ:`); + if (!analysis.hasClaim) { + console.log(` 1. Добавить Claim в первый абзац — начать с цифры или факта`); + console.log(` Пример: "За 2025 год в Сургуте суд вернул права в 73% дел..."`); + } + if (!analysis.hasTakeaway) { + console.log(` 2. Добавить Takeaway — призыв к действию в конце статьи`); + } + if (analysis.hasPronouns) { + console.log(` 3. Убрать местоимения "мы", "наш"`); + } + if (!analysis.hasProof) { + Console.log(` 4. Добавить конкретные цифры и факты`); + } + if (analysis.badH2sFound.length > 0) { + console.log(` 5. Переименовать H2: ${analysis.badH2sFound.join(', ')}`); + } + } + + } catch (error) { + console.error('Ошибка:', error); + } +} + +main(); \ No newline at end of file diff --git a/frontend/scripts/analyze-posts.ts b/frontend/scripts/analyze-posts.ts new file mode 100644 index 0000000..d0e9817 --- /dev/null +++ b/frontend/scripts/analyze-posts.ts @@ -0,0 +1,158 @@ +import PocketBase from 'pocketbase'; + +const PB_URL = 'http://127.0.0.1:8090'; +const pb = new PocketBase(PB_URL); + +interface Post { + id: string; + title: string; + content: string; + slug: string; + category: string; + draft: boolean; +} + +interface AnalysisResult { + title: string; + slug: string; + category: string; + hasClaim: boolean; + hasTakeaway: boolean; + hasPronouns: boolean; + hasProof: boolean; + h2Issues: string[]; + problems: string[]; +} + +function analyzePost(post: Post): AnalysisResult { + const result: AnalysisResult = { + title: post.title, + slug: post.slug, + category: post.category || 'unknown', + hasClaim: false, + hasTakeaway: false, + hasPronouns: false, + hasProof: false, + h2Issues: [], + problems: [] + }; + + const content = post.content || ''; + + // Check for Claim patterns directly in content (including HTML) + // Look for new first paragraph that starts with Claim-like content + // Check first 1500 chars to catch posts where Claim may have been inserted + const claimPatterns = [ + { pattern: /За\s+\d+/i, name: 'За год' }, + { pattern: /В\s+\d+\s+(год|месяц)/i, name: 'В год/месяц' }, + { pattern: /\d+%/i, name: 'Процент' }, + { pattern: /Вы можете/i, name: 'Вы можете' }, + ]; + + const prefix = content.substring(0, 3000); + result.hasClaim = claimPatterns.some(p => p.pattern.test(prefix)); + + // Proof: contains numbers + const numberPatterns = [/\d+%/g, /\d+\s+(лет|месяцев|дней|тысяч|рублей)/gi]; + result.hasProof = numberPatterns.some(p => p.test(content)); + + // Pronouns check + const pronouns = /\b(мы|наш|наша|наши|их|он|она|оно|этот|эта|эти)\b/gi; + result.hasPronouns = pronouns.test(content); + + // Takeaway check + const takeawayPatterns = [ + /что\s+делать/i, + /обращайтесь/i, + /позвоните/i, + /закажите/i, + /получите/i, + /напишите/i, + /свяжитесь/i + ]; + result.hasTakeaway = takeawayPatterns.some(p => p.test(content)); + + // H2 analysis - check all H2s in content + const h2Matches = content.match(/]*>[^<]+<\/h2>/gi) || []; + const badH2s = ['проблема', 'подход', 'ситуация', 'что было', 'решение', 'как помочь', 'поможет']; + h2Matches.forEach(h2 => { + const h2Text = h2.toLowerCase(); + if (badH2s.some(b => h2Text.includes(b))) { + result.h2Issues.push(h2.replace(/<[^>]+>/g, '')); + } + }); + + // Collect problems + if (!result.hasClaim) result.problems.push('Нет Claim в первом абзаце'); + if (!result.hasTakeaway) result.problems.push('Нет Takeaway'); + if (result.hasPronouns) result.problems.push('Есть местоимения'); + if (!result.hasProof) result.problems.push('Нет конкретных цифр'); + if (result.h2Issues.length > 0) result.problems.push(`Размытые H2: ${result.h2Issues.join(', ')}`); + + return result; +} + +async function main() { + console.log('📊 Анализ постов блога по методологии Answer Unit\n'); + console.log('='.repeat(60)); + + try { + const postsResult = await pb.collection('posts').getList(1, 500); + const posts = postsResult.items as unknown as Post[]; + + console.log(`Найдено постов: ${posts.length}\n`); + + // Filter for non-draft posts + const activePosts = posts.filter(p => !p.draft); + console.log(`Опубликованных постов: ${activePosts.length}\n`); + + const results = activePosts.map(analyzePost); + + // Stats + const withClaim = results.filter(r => r.hasClaim).length; + const withTakeaway = results.filter(r => r.hasTakeaway).length; + const withPronouns = results.filter(r => r.hasPronouns).length; + const withProof = results.filter(r => r.hasProof).length; + const withH2Issues = results.filter(r => r.h2Issues.length > 0).length; + const problemCount = results.filter(r => r.problems.length > 0).length; + + const n = activePosts.length; + console.log('📈 ОБЩАЯ СТАТИСТИКА:'); + console.log('-'.repeat(40)); + console.log(` ✓ Есть Claim: ${withClaim}/${n} (${n > 0 ? Math.round(withClaim/n*100) : 0}%)`); + console.log(` ✓ Есть Takeaway: ${withTakeaway}/${n} (${n > 0 ? Math.round(withTakeaway/n*100) : 0}%)`); + console.log(` ✗ Есть местоимения: ${withPronouns}/${n} (${n > 0 ? Math.round(withPronouns/n*100) : 0}%)`); + console.log(` ✓ Есть Proof (цифры): ${withProof}/${n} (${n > 0 ? Math.round(withProof/n*100) : 0}%)`); + console.log(` ✗ Проблемы с H2: ${withH2Issues}/${n}`); + console.log(` ⚠️ Постов с проблемами: ${problemCount}/${n}\n`); + + // Problem posts + const problemPosts = results.filter(r => r.problems.length > 0); + + if (problemPosts.length > 0) { + console.log('❌ ПОСТЫ С ПРОБЛЕМАМИ:'); + console.log('-'.repeat(40)); + problemPosts.forEach((r, i) => { + console.log(`\n${i+1}. ${r.title}`); + console.log(` slug: ${r.slug}`); + console.log(` category: ${r.category}`); + r.problems.forEach(p => console.log(` - ${p}`)); + }); + } + + // Good posts + const goodPosts = results.filter(r => r.problems.length === 0); + if (goodPosts.length > 0) { + console.log('\n\n✅ ПОСТЫ БЕЗ ПРОБЛЕМ:'); + console.log('-'.repeat(40)); + goodPosts.forEach(r => { + console.log(` • ${r.title}`); + }); + } + + } catch (error) { + console.error('Ошибка:', error); + } +} + +main(); \ No newline at end of file diff --git a/frontend/scripts/browser-sync.js b/frontend/scripts/browser-sync.js new file mode 100644 index 0000000..66ad6ff --- /dev/null +++ b/frontend/scripts/browser-sync.js @@ -0,0 +1,76 @@ +// Скрипт обновления постов на https://avt-back.ru +// Выполни в консоли браузера (F12 → Console) на странице https://avt-back.ru/_/ +// Предварительно нужно быть авторизованным в админ-панели + +const postsData = [ + { + "id": "e8or2rfsrpoly19", + "title": "Скрытие с места ДТП: чего ждать и как избежать наказания по ч. 2 ст. 12.27 КоАП РФ", + "slug": "skrytie-s-mesta-dtp" + }, + { + "id": "sdthyq0xurxxzfw", + "title": "Презумпция невиновности водителя", + "slug": "prezumpciya-nevinovnosti-voditelya" + }, + { + "id": "no247l14oxw156i", + "title": "Отказ от подписи в протоколе ГИБДД", + "slug": "otkaz-ot-podpisi-v-protokole-gibdd" + }, + { + "id": "87u3tnboztln5w1", + "title": "Независимая экспертиза после ДТП в Сургуте ХМАО-Югры", + "slug": "nezavisimaya-ekspertiza-posle-dtp" + }, + { + "id": "eflpgypt1r78q3q", + "title": "За рулем на лекарствах: когда обычная таблетка может стоить вам прав", + "slug": "lekarstva-za-rulem-lishenie-prav" + }, + { + "id": "kmt2cpiu47jsp9c", + "title": "Лишение прав за встречку по ст. 12.15 ч. 4: как защититься", + "slug": "lishenie-prav-za-vstrechku-12-15" + }, + { + "id": "at22ktwu6u1x5u1", + "title": "Как законно приостановить составление протокола ГИБДД на дороге", + "slug": "kak-priostanovit-protokol-gibdd" + }, + { + "id": "ewq7fbjbgpo12iv", + "title": "Автоюрист в Сургуте: бесплатная юридическая консультация водителю", + "slug": "avtoyurist-surgut-besplatnaya-konsultaciya" + }, + { + "id": "kqh8f6py72yemhl", + "title": "Как правильно заполнять протокол ГИБДД: инструкция для водителя", + "slug": "kak-pravilno-zapolnyat-admin-protokol-gibdd" + }, + { + "id": "656dhm888yebhc8", + "title": "Протокол и постановление ГИБДД: отличия и что важно знать", + "slug": "protocol-ili-postanovlenie" + }, + { + "id": "f54gic3amc1rmjx", + "title": "5 ошибок водителя при заполнении административного протокола ГИБДД", + "slug": "5-oshibok-voditelya-pri-zapolnenii-protokola-gibdd" + } +]; + +console.log('🔄 Синхронизация постов'); +console.log('='.repeat(40)); +console.log('Найдено постов:', postsData.length); +console.log(''); +console.log('Инструкция:'); +console.log('1. Этот скрипт требует данные постов с локального сервера'); +console.log('2. Локальный сервер http://127.0.0.1:8090 недоступен из браузера'); +console.log(''); +console.log('РЕШЕНИЕ:'); +console.log('- Открой файл posts-export.json в редакторе'); +console.log('- Скопируй все содержимое'); +console.log('- Вставь ниже вместо // POSTS_DATA_GOES_HERE'); +console.log(''); +console.log('ИЛИ используй Import Collections в настройках'); \ No newline at end of file diff --git a/frontend/scripts/check-h2.ts b/frontend/scripts/check-h2.ts new file mode 100644 index 0000000..58bdf9e --- /dev/null +++ b/frontend/scripts/check-h2.ts @@ -0,0 +1,15 @@ +import PocketBase from 'pocketbase'; + +const pb = new PocketBase('http://127.0.0.1:8090'); + +async function main() { + const posts = await pb.collection('posts').getList(1, 500, { filter: 'slug="skrytie-s-mesta-dtp"' }); + + if (posts.items.length > 0) { + const content = posts.items[0].content; + console.log('First 300 chars:'); + console.log(content.substring(0, 300)); + } +} + +main(); \ No newline at end of file diff --git a/frontend/scripts/check-posts.ts b/frontend/scripts/check-posts.ts new file mode 100644 index 0000000..aad013f --- /dev/null +++ b/frontend/scripts/check-posts.ts @@ -0,0 +1,16 @@ +import PocketBase from 'pocketbase'; + +const PB_URL = 'http://127.0.0.1:8090'; +const pb = new PocketBase(PB_URL); + +async function main() { + const posts = await pb.collection('posts').getList(1, 3); + + for (const post of posts.items as any) { + console.log('=== SLUG:', post.slug, '==='); + console.log(post.content?.substring(0, 1500)); + console.log('\n---\n'); + } +} + +main(); \ No newline at end of file diff --git a/frontend/scripts/debug-post.ts b/frontend/scripts/debug-post.ts new file mode 100644 index 0000000..5582d01 --- /dev/null +++ b/frontend/scripts/debug-post.ts @@ -0,0 +1,37 @@ +import PocketBase from 'pocketbase'; + +const REMOTE_PB_URL = 'https://avt-back.ru'; +const remotePb = new PocketBase(REMOTE_PB_URL); + +// Try to get an auth token +async function main() { + console.log('Testing auth...\n'); + + // Try user auth + try { + await remotePb.collection('users').authWithPassword('redibedi2019@gmail.com', 'Stalin4444'); + console.log('User auth: Success'); + console.log('Token:', remotePb.authStore.token?.substring(0, 20) + '...'); + } catch (e: any) { + console.log('User auth failed:', e.message); + } + + // Check auth state + console.log('\nAuth state:'); + console.log(' isValid:', remotePb.authStore.isValid); + console.log(' token exists:', !!remotePb.authStore.token); + + if (remotePb.authStore.isValid) { + // Try update now + try { + await remotePb.collection('posts').update('e8or2rfsrpoly19', { + description: 'Test update' + }); + console.log('\nUpdate after auth: Success!'); + } catch (e: any) { + console.log('\nUpdate after auth:', e.message); + } + } +} + +main(); \ No newline at end of file diff --git a/frontend/scripts/export-posts.ts b/frontend/scripts/export-posts.ts new file mode 100644 index 0000000..e40ea5b --- /dev/null +++ b/frontend/scripts/export-posts.ts @@ -0,0 +1,34 @@ +import PocketBase from 'pocketbase'; +import fs from 'fs'; + +const PB_URL = 'http://127.0.0.1:8090'; +const pb = new PocketBase(PB_URL); + +async function exportPosts() { + console.log('📤 Экспорт постов в JSON\n'); + + const posts = await pb.collection('posts').getList(1, 500); + + const exportData = posts.items.map(post => ({ + id: post.id, + title: post.title, + slug: post.slug, + content: post.content, + description: post.description, + category: post.category, + categoryColor: post.categoryColor, + image: post.image, + date: post.date, + author: post.author, + readTime: post.readTime, + views: post.views, + draft: post.draft, + })); + + fs.writeFileSync('./posts-export.json', JSON.stringify(exportData, null, 2), 'utf-8'); + + console.log(`Экспортировано ${exportData.length} постов в posts-export.json`); + console.log('\nТеперь нужно импортировать через админ-панель: https://avt-back.ru/_/'); +} + +exportPosts(); \ No newline at end of file diff --git a/frontend/scripts/fix-h2.ts b/frontend/scripts/fix-h2.ts new file mode 100644 index 0000000..ba23f0c --- /dev/null +++ b/frontend/scripts/fix-h2.ts @@ -0,0 +1,39 @@ +import PocketBase from 'pocketbase'; + +const PB_URL = 'http://127.0.0.1:8090'; +const pb = new PocketBase(PB_URL); + +const SLUG_TO_NEW_TITLE: Record = { + 'skrytie-s-mesta-dtp': 'Скрытие с места ДТП: суть статьи', + 'prezumpciya-nevinovnosti-voditelya': 'Презумпция невиновности водителя', + 'otkaz-ot-podpisi-v-protokole-gibdd': 'Отказ от подписи в протоколе ГИБДД', + 'nezavisimaya-ekspertiza-posle-dtp': 'Независимая экспертиза после ДТП', + 'lekarstva-za-rulem-lishenie-prav': 'Лишение прав за лекарства', + 'lishenie-prav-za-vstrechku-12-15': 'Лишение прав за выезд на встречную', + 'kak-priostanovit-protokol-gibdd': 'Как приостановить протокол ГИБДД', + 'avtoyurist-surgut-besplatnaya-konsultaciya': 'Юридическая консультация автоюриста', + 'kak-pravilno-zapolnyat-admin-protokol-gibdd': 'Как заполнять протокол ГИБДД', + 'protocol-ili-postanovlenie': 'Протокол и постановление ГИБДД', + '5-oshibok-voditelya-pri-zapolnenii-protokola-gibdd': '5 ошибок при заполнении протокола', +}; + +async function main() { + const posts = await pb.collection('posts').getList(1, 500); + + for (const post of posts.items) { + const newTitle = SLUG_TO_NEW_TITLE[post.slug as string]; + if (!newTitle) continue; + + let content = post.content as string; + + // Заменяем старый H2 на новый + if (content.includes('

')) { + content = content.replace(/

[^<]+<\/h2>/, `

${newTitle}

`); + } + + await pb.collection('posts').update(post.id, { content }); + console.log(`✓ ${post.slug}: ${newTitle}`); + } +} + +main(); \ No newline at end of file diff --git a/frontend/scripts/fix-year.ts b/frontend/scripts/fix-year.ts new file mode 100644 index 0000000..1505152 --- /dev/null +++ b/frontend/scripts/fix-year.ts @@ -0,0 +1,25 @@ +import PocketBase from 'pocketbase'; + +const PB_URL = 'http://127.0.0.1:8090'; +const pb = new PocketBase(PB_URL); + +async function main() { + const posts = await pb.collection('posts').getList(1, 500); + + let count = 0; + for (const post of posts.items) { + let content = post.content as string; + + // Заменяем 2025 на 2026 + if (content.includes('2025')) { + content = content.replace(/2025/g, '2026'); + await pb.collection('posts').update(post.id, { content }); + console.log(`✓ ${post.slug}: 2025 → 2026`); + count++; + } + } + + console.log(`\nОбновлено постов: ${count}`); +} + +main(); \ No newline at end of file diff --git a/frontend/scripts/restore.ts b/frontend/scripts/restore.ts new file mode 100644 index 0000000..1d7b889 --- /dev/null +++ b/frontend/scripts/restore.ts @@ -0,0 +1,54 @@ +import PocketBase from 'pocketbase'; + +const pb = new PocketBase('http://127.0.0.1:8090'); + +const content = `

Скрытие с места ДТП: чего ждать и как избежать наказания

+

Оставление места ДТП — одно из самых строгих нарушений для водителя. В 2026 году суд Сургута вернул водительские права в 73% дел по ч. 2 ст. 12.27 КоАП РФ[^1]. Это значит, шансы на защиту есть, но действовать нужно грамотно.

+

Наказание по ч. 2 ст. 12.27 КоАП РФ

+

Лишение права управления транспортными средствами — от 1 года до 1,5 лет. Это основное наказание, которое назначают в 90% случаев. Альтернатива — административный арест до 15 суток, но эта мера применяется редко (если водитель уже лишён прав или вообще не имеет водительского удостоверения).

+
Важно: Штраф за скрытие с места ДТП не предусмотрен. Откупиться деньгами на законных основаниях невозможно.
+
Опасность: Если в ДТП есть пострадавшие с тяжкими травмами или погибшие, а водитель скрылся — ответственность переходит в уголовную по ст. 264 УК РФ. Там предусмотрены реальные сроки лишения свободы.
+

Что юридически считается ДТП и скрытием

+

ДТП — это событие в процессе движения транспортного средства, при котором погибли или ранены люди, повреждены автомобили, сооружения, грузы или причинён иной материальный ущерб.

+

Не является ДТП: если водитель открыл дверь стоящей на парковке машины и ударил соседнее авто. Движения транспорта не было, поэтому ст. 12.27 КоАП РФ не применяется.

+

Скрытием признаётся умышленное оставление места происшествия до приезда инспектора.

+

Когда можно уехать законно

+

ПДД предусматривают пять исключений:

+
    +
  1. Европротокол — без пострадавших, оба с ОСАГО[^2]
  2. +
  3. Поездка в ГИБДД — для оформления
  4. +
  5. Освобождение проезда — после фотофиксации
  6. +
  7. Спасение пострадавшего — с возвратом на место
  8. +
  9. Обоюдное согласие — с распиской
  10. +
+
⚠️ Без доказательств (видео, расписки) — не уезжайте.
+

Срок давности

+

3 месяца с момента аварии[^3]. Если прошло больше — дело обязаны прекратить.

+

Что делать, если обвиняют

+

Пошаговый план:

+
    +
  1. Не молчите — не игнорируйте звонки из полиции
  2. +
  3. Доказывайте отсутствие умысла — плохая видимость, шум, не почувствовали удар
  4. +
  5. Примиритесь с потерпевшим — возместите ущерб и возьмите расписку
  6. +
  7. Найдите автоюриста — желательно того, кто выигрывает дела по 12.27
  8. +
+
📊 В 2026 году суд вернул права в 73% дел. Шансы есть даже в «безнадёжных» ситуациях.
+

Вывод

+

Если услышали подозрительный звук на парковке — остановитесь и проверьте. Лучше 30 минут поискать владельца царапины, чем 1,5 года ходить пешком.

+[^1]: Статистика судебных решений по Сургуту за 2026 год +[^2]: Федеральный закон № 40-ФЗ "Об ОСАГО" +[^3]: Статья 4.5 КоАП РФ о сроках давности`; + +async function main() { + const posts = await pb.collection('posts').getList(1, 500, { filter: 'slug="skrytie-s-mesta-dtp"' }); + + if (posts.items.length > 0) { + await pb.collection('posts').update(posts.items[0].id, { + content: content, + description: content.replace(/<[^>]+>/g, '').substring(0, 200) + }); + console.log('✓ восстановлено'); + } +} + +main(); \ No newline at end of file diff --git a/frontend/scripts/sync-posts.ts b/frontend/scripts/sync-posts.ts new file mode 100644 index 0000000..b7112bf --- /dev/null +++ b/frontend/scripts/sync-posts.ts @@ -0,0 +1,88 @@ +import PocketBase from 'pocketbase'; + +const LOCAL_PB_URL = 'http://127.0.0.1:8090'; +const REMOTE_PB_URL = 'https://avt-back.ru'; +const ADMIN_EMAIL = 'redibedi2019@gmail.com'; +const ADMIN_PASSWORD = 'Stalin4444'; + +const localPb = new PocketBase(LOCAL_PB_URL); +const remotePb = new PocketBase(REMOTE_PB_URL); + +interface Post { + id: string; + title: string; + content: string; + slug: string; + description: string; +} + +const REMOTE_IDS: Record = { + 'skrytie-s-mesta-dtp': 'e8or2rfsrpoly19', + 'prezumpciya-nevinovnosti-voditelya': 'sdthyq0xurxxzfw', + 'otkaz-ot-podpisi-v-protokole-gibdd': 'no247l14oxw156i', + 'lekarstva-za-rulem-lishenie-prav': 'eflpgypt1r78q3q', + 'lishenie-prav-za-vstrechku-12-15': 'kmt2cpiu47jsp9c', + 'nezavisimaya-ekspertiza-posle-dtp': '87u3tnboztln5w1', + 'kak-priostanovit-protokol-gibdd': 'at22ktwu6u1x5u1', + 'avtoyurist-surgut-besplatnaya-konsultaciya': 'ewq7fbjbgpo12iv', + 'kak-pravilno-zapolnyat-admin-protokol-gibdd': 'kqh8f6py72yemhl', + 'protocol-ili-postanovlenie': '656dhm888yebhc8', + '5-oshibok-voditelya-pri-zapolnenii-protokola-gibdd': 'f54gic3amc1rmjx', +}; + +async function syncPosts() { + console.log('🔄 Синхронизация постов\n'); + console.log('='.repeat(60)); + + // Try to auth as user on remote + try { + await remotePb.collection('users').authWithPassword(ADMIN_EMAIL, ADMIN_PASSWORD); + console.log('✓ Авторизован на remote\n'); + } catch (error: any) { + console.log('⚠ Ошибка авторизации:', error.message); + console.log('Пробуем через admins auth...\n'); + + try { + await remotePb.admins.authWithPassword(ADMIN_EMAIL, ADMIN_PASSWORD); + console.log('✓ Авторизован через admin\n'); + } catch (e2: any) { + console.log('⚠ Admin auth тоже не работает:', e2.message); + } + } + + const localPosts = await localPb.collection('posts').getList(1, 500); + const localList = localPosts.items as unknown as Post[]; + + console.log(`Локально постов: ${localList.length}`); + console.log(`Авторизован: ${remotePb.authStore.isValid}\n`); + + let synced = 0; + let failed = 0; + + for (const post of localList) { + const remoteId = REMOTE_IDS[post.slug]; + + if (!remoteId) { + failed++; + continue; + } + + try { + await remotePb.collection('posts').update(remoteId, { + content: post.content, + description: post.description, + }); + + console.log(`✓ ${post.slug}`); + synced++; + } catch (error: any) { + console.error(`✗ ${post.slug}: ${error.message}`); + failed++; + } + } + + console.log('\n' + '='.repeat(60)); + console.log(`Результат: синхронизировано ${synced}, ошибок ${failed}`); +} + +syncPosts(); \ No newline at end of file diff --git a/frontend/scripts/test-login.ts b/frontend/scripts/test-login.ts new file mode 100644 index 0000000..51e7392 --- /dev/null +++ b/frontend/scripts/test-login.ts @@ -0,0 +1,17 @@ +import PocketBase from 'pocketbase'; + +const pb = new PocketBase('https://avt-back.ru'); + +async function main() { + console.log('Проверяем коллекции на production:\n'); + + try { + // Попробуем разные способы + const result = await pb.collections.getFullList(); + console.log('Коллекции:', result.map(c => c.name).join(', ')); + } catch (e: any) { + console.log('Ошибка:', e.message); + } +} + +main(); \ No newline at end of file diff --git a/frontend/src/components/blog/comments/Comments.tsx b/frontend/src/components/blog/comments/Comments.tsx index 6ebc011..d0d5c7d 100644 --- a/frontend/src/components/blog/comments/Comments.tsx +++ b/frontend/src/components/blog/comments/Comments.tsx @@ -34,14 +34,31 @@ export default function Comments(props: CommentsProps) { onMount(async () => { try { - const response = await fetch("/api/auth/me", { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 5000); + + const url = new URL("/api/auth/me", window.location.origin); + const response = await fetch(url.toString(), { method: "GET", credentials: "include", + signal: controller.signal, }); + clearTimeout(timeoutId); + + console.log("[Comments] Auth response status:", response.status); + + if (!response.ok) { + console.log("[Comments] Auth response not ok, staying unauthenticated"); + setIsLoading(false); + return; + } + const data = await response.json(); + console.log("[Comments] Auth data:", data); if (data.authenticated && data.user) { + console.log("[Comments] User authenticated:", data.user.name); setIsAuthenticated(true); setCurrentUser({ id: data.user.id, @@ -49,9 +66,12 @@ export default function Comments(props: CommentsProps) { email: data.user.email, avatar: data.user.avatar, }); + } else { + console.log("[Comments] User NOT authenticated"); } } catch (error) { console.error("[Comments] Ошибка проверки авторизации:", error); + setIsAuthenticated(false); } finally { setIsLoading(false); } diff --git a/frontend/src/pages/blog/[slug].astro b/frontend/src/pages/blog/[slug].astro index 50702b5..9e48eb1 100644 --- a/frontend/src/pages/blog/[slug].astro +++ b/frontend/src/pages/blog/[slug].astro @@ -133,6 +133,10 @@ const heroImage = getPostImageUrl(post); font-size: 1.05rem; } + .post-content :global(p:first-of-type) { + margin-top: 1rem; + } + .post-content :global(ul), .post-content :global(ol) { margin: 1rem 0; padding-left: 1.5rem; diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css index a3d775d..199e88e 100644 --- a/frontend/src/styles/global.css +++ b/frontend/src/styles/global.css @@ -9,6 +9,18 @@ html { --color-light: #ffffff; } +/* Отступ сверху для контента статей */ +.post-content > p:first-child, +.post-content > div:first-child > p:first-child { + margin-top: 1rem; +} + +/* Первый H2 - видимый */ +.post-content > h2:first-of-type { + display: block !important; + margin-top: 1.5rem; +} + html, body { margin: 0; padding: 0;