Отзывы и предложения к софту от AleXStam
Поговорим о...
Fff
Прикрепления:
8753146.zip (13.8 Kb)
444
Прикрепления:
344433.zip (16.1 Kb)
<script>
document.addEventListener('DOMContentLoaded', function() {
function handleAnchorScroll() {
if (window.location.hash) {
const element = document.querySelector(window.location.hash);
if (element) {
const headerHeight = 80; // Подставьте высоту вашего хедера
const elementPosition = element.getBoundingClientRect().top;
const offsetPosition = elementPosition + window.pageYOffset - headerHeight;

window.scrollTo({
top: offsetPosition,
behavior: 'smooth'
});
}
}
}

// Запускаем сразу
handleAnchorScroll();

// И при изменении хеша
window.addEventListener('hashchange', handleAnchorScroll);
});
</script>
<style>
html {
scroll-padding-top: var(--header-height, 80px);
scroll-behavior: smooth;
}
</style>

<script>
function updateHeaderHeight() {
const header = document.querySelector('header, .site-header, nav');
if (header) {
const height = header.offsetHeight;
document.documentElement.style.setProperty('--header-height', height + 'px');
return height;
}
return 80; // fallback
}

document.addEventListener('DOMContentLoaded', function() {
// Обновляем высоту при загрузке и ресайзе
updateHeaderHeight();
window.addEventListener('resize', updateHeaderHeight);

// Обработка якорных ссылок
function scrollToAnchor() {
if (window.location.hash) {
const element = document.querySelector(window.location.hash);
if (element) {
const headerHeight = updateHeaderHeight();
setTimeout(() => {
const y = element.getBoundingClientRect().top + window.pageYOffset - headerHeight;
window.scrollTo({ top: y, behavior: 'smooth' });
}, 150);
}
}
}

setTimeout(scrollToAnchor, 100);
window.addEventListener('hashchange', scrollToAnchor);
});
</script>
Понял! Проблема в том, что при изменении масштаба браузера сбиваются расчеты позиций. Браузерные зуммы ломают getBoundingClientRect() и другие методы расчета позиций.

Вот работающее решение:

1. Упрощенный надежный вариант

```html
<script>
document.addEventListener('DOMContentLoaded', function() {
function scrollToAnchor() {
if (window.location.hash) {
const element = document.querySelector(window.location.hash);
if (element) {
// Простой и надежный способ - используем встроенный метод
// с дополнительной задержкой для применения масштаба
setTimeout(() => {
element.scrollIntoView({
behavior: 'smooth',
block: 'start'
});

// Дополнительная корректировка после скролла
setTimeout(() => {
const header = document.querySelector('header, .header, nav');
if (header) {
const headerHeight = header.offsetHeight;
if (window.scrollY < element.offsetTop - headerHeight) {
window.scrollTo(0, element.offsetTop - headerHeight);
}
}
}, 300);
}, 100);
}
}
}

// Запускаем с задержкой для стабилизации масштаба
setTimeout(scrollToAnchor, 200);
window.addEventListener('hashchange', scrollToAnchor);
});
</script>
```

2. Решение с принудительным пересчетом

```html
<script>
function forceScrollToAnchor() {
if (window.location.hash) {
const element = document.querySelector(window.location.hash);
if (element) {
// Принудительно сбрасываем скролл сначала в начало
window.scrollTo(0, 0);

// Затем скроллим к нужному элементу с учетом хедера
setTimeout(() => {
const header = document.querySelector('header');
const headerHeight = header ? header.offsetHeight : 80;
const targetY = element.offsetTop - headerHeight;

window.scrollTo({
top: targetY,
behavior: 'smooth'
});

// Дублирующая проверка через время
setTimeout(() => {
const currentPos = window.scrollY;
const expectedPos = element.offsetTop - headerHeight;
if (Math.abs(currentPos - expectedPos) > 50) {
window.scrollTo(0, expectedPos);
}
}, 400);
}, 50);
}
}
}

document.addEventListener('DOMContentLoaded', function() {
setTimeout(forceScrollToAnchor, 300);
window.addEventListener('hashchange', forceScrollToAnchor);
});
</script>
```

3. Решение без smooth scroll (самое надежное)

```html
<script>
function reliableAnchorScroll() {
if (window.location.hash) {
// Убираем smooth scroll - он часто ломается при зуме
const element = document.querySelector(window.location.hash);
if (element) {
// Метод 1: используем offsetTop (менее подвержен влиянию масштаба)
const header = document.querySelector('header');
const headerHeight = header ? header.offsetHeight : 80;
const targetScroll = Math.max(0, element.offsetTop - headerHeight);

// Мгновенный скролл
window.scrollTo(0, targetScroll);

// Проверка и коррекция
setTimeout(() => {
if (Math.abs(window.scrollY - targetScroll) > 10) {
window.scrollTo(0, targetScroll);
}
}, 100);
}
}
}

// Обработчик для всех случаев
function initAnchorScroll() {
// Ждем полной загрузки и стабилизации
setTimeout(reliableAnchorScroll, 400);
}

document.addEventListener('DOMContentLoaded', initAnchorScroll);
window.addEventListener('hashchange', reliableAnchorScroll);
window.addEventListener('load', initAnchorScroll);
</script>
```

4. CSS + JS комбо (рекомендую этот)

```css
/* Отключаем smooth scroll для якорных ссылок */
html {
scroll-behavior: auto; /* или не указывать вообще */
}

/* Жесткие отступы для :target */
:target {
scroll-margin-top: 100px; /* Задайте с запасом */
}

@media (max-width: 1920px) {
:target {
scroll-margin-top: 80px;
}
}
```

```html
<script>
function zoomResistantAnchor() {
if (window.location.hash) {
const hash = window.location.hash;
const element = document.querySelector(hash);

if (element) {
// Временно убираем хеш из URL
window.location.hash = "";

// Даем время браузеру обработать
setTimeout(() => {
// Используем самый простой метод
element.scrollIntoView();

// Ручная корректировка позиции
setTimeout(() => {
const header = document.querySelector('header');
if (header) {
const currentScroll = window.scrollY;
const elementTop = element.getBoundingClientRect().top + currentScroll;
const headerHeight = header.offsetHeight;

if (currentScroll > elementTop - headerHeight - 10) {
window.scrollTo(0, Math.max(0, elementTop - headerHeight));
}
}

// Возвращаем хеш в URL
window.location.hash = hash;
}, 50);
}, 10);
}
}
}

document.addEventListener('DOMContentLoaded', function() {
// Большая задержка для стабилизации масштаба
setTimeout(zoomResistantAnchor, 500);
});
window.addEventListener('hashchange', zoomResistantAnchor);
</script>
```

5. Экстренное решение - убрать все сложности

```html
<script>
// Максимально простое решение
document.addEventListener('DOMContentLoaded', function() {
setTimeout(function() {
if (window.location.hash) {
const element = document.querySelector(window.location.hash);
if (element) {
// Просто скроллим и все
element.scrollIntoView();

// На всякий случай дублируем
setTimeout(() => element.scrollIntoView(), 100);
}
}
}, 300);
});
</script>
```

Попробуйте сначала вариант 3 - он самый надежный против проблем с масштабом. Если не поможет, то вариант 4.

Также проверьте - возможно у вас в CSS есть transform: scale() или другие трансформации, которые влияют на расчет позиций?
<script>
function universalAnchorScroll() {
if (window.location.hash) {
const element = document.querySelector(window.location.hash);
if (element) {
// Сначала скроллим просто к элементу
element.scrollIntoView();

// Затем корректируем позицию с учетом хедера
setTimeout(() => {
const header = document.querySelector('header, .header, nav, [class*="header"], [class*="nav"]');
let headerHeight = 80;

if (header) {
headerHeight = header.offsetHeight;
console.log('Found header height:', headerHeight);
}

// Сильная коррекция - пробуем разные варианты
const currentScroll = window.scrollY;
const elementRect = element.getBoundingClientRect();
const elementTop = elementRect.top + currentScroll;

// Пробуем скролл с разными отступами
const targetPosition = elementTop - headerHeight - 20; // дополнительный отступ

window.scrollTo(0, targetPosition);

// Финальная проверка и коррекция
setTimeout(() => {
const finalPosition = window.scrollY;
if (Math.abs(finalPosition - targetPosition) > 30) {
window.scrollTo(0, elementTop - headerHeight - 10);
}
}, 150);

}, 50);
}
}
}

document.addEventListener('DOMContentLoaded', function() {
setTimeout(universalAnchorScroll, 500);
});
window.addEventListener('hashchange', universalAnchorScroll);
</script>
Uncaught SyntaxError: Failed to execute 'querySelector' on 'Document': '#%D1%81%D1%82%D1%80%D1%83%D0%BA%D1%82%D1%83%D1%80%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%BD%D0%BE%D0%B5-%D0%BE%D0%BF%D0%B8%D1%81%D0%B0%D0%BD%D0%B8%D0%B5-%D0%B8%D0%BD%D1%82%D0%B5%D1%80%D0%B2%D0%B0%D0%BB%D0%B0' is not a valid selector.
at universalAnchorScroll (main.js:57:28)
<script>
function universalAnchorScroll() {
if (window.location.hash) {
// Декодируем URL и убираем # для querySelector
const hash = decodeURIComponent(window.location.hash.substring(1));

try {
const element = document.getElementById(hash);
if (element) {
// Сначала скроллим просто к элементу
element.scrollIntoView();

// Затем корректируем позицию с учетом хедера
setTimeout(() => {
const header = document.querySelector('header, .header, nav, [class*="header"], [class*="nav"]');
let headerHeight = 80;

if (header) {
headerHeight = header.offsetHeight;
console.log('Found header height:', headerHeight);
}

// Сильная коррекция
const currentScroll = window.scrollY;
const elementRect = element.getBoundingClientRect();
const elementTop = elementRect.top + currentScroll;

// Пробуем скролл с разными отступами
const targetPosition = elementTop - headerHeight - 20;

window.scrollTo(0, targetPosition);

// Финальная проверка и коррекция
setTimeout(() => {
const finalPosition = window.scrollY;
if (Math.abs(finalPosition - targetPosition) > 30) {
window.scrollTo(0, elementTop - headerHeight - 10);
}
}, 150);

}, 50);
} else {
console.log('Element not found with id:', hash);
}
} catch (e) {
console.error('Error in anchor scroll:', e);
}
}
}

document.addEventListener('DOMContentLoaded', function() {
setTimeout(universalAnchorScroll, 500);
});
window.addEventListener('hashchange', universalAnchorScroll);
</script>
Отлично, что работает! А то, что сначала некорректно, а потом выравнивается - это нормально. Это происходит потому, что:

1. Браузер сначала пытается сам проскроллить к якорю
2. Потом наш скрипт исправляет позицию

Вот варианты как это улучшить:

1. Ускорить исправление (уменьшить задержки)

```html
<script>
function fastAnchorScroll() {
if (window.location.hash) {
const hash = decodeURIComponent(window.location.hash.substring(1));

try {
const element = document.getElementById(hash);
if (element) {
// Убираем начальный scrollIntoView - сразу корректируем
const header = document.querySelector('header, .header, nav');
let headerHeight = header ? header.offsetHeight : 80;

// Немедленный скролл к правильной позиции
const targetPosition = Math.max(0, element.offsetTop - headerHeight - 10);
window.scrollTo(0, targetPosition);

// Быстрая коррекция через минимальное время
setTimeout(() => {
window.scrollTo(0, targetPosition);
}, 50);

// Финальная тонкая настройка
setTimeout(() => {
const currentPos = window.scrollY;
if (Math.abs(currentPos - targetPosition) > 5) {
window.scrollTo(0, targetPosition);
}
}, 150);
}
} catch (e) {
console.error('Error in anchor scroll:', e);
}
}
}

// Уменьшаем задержку перед запуском
document.addEventListener('DOMContentLoaded', function() {
setTimeout(fastAnchorScroll, 100); // было 500, стало 100
});
window.addEventListener('hashchange', fastAnchorScroll);
</script>
```

2. Предотвратить браузерный скролл

```html
<script>
function preventBrowserAnchorScroll() {
if (window.location.hash) {
// Временно убираем хеш чтобы браузер не скроллил
const hash = window.location.hash;
history.replaceState(null, null, ' ');

const decodedHash = decodeURIComponent(hash.substring(1));
const element = document.getElementById(decodedHash);

if (element) {
const header = document.querySelector('header');
const headerHeight = header ? header.offsetHeight : 80;
const targetPosition = Math.max(0, element.offsetTop - headerHeight);

// Сразу скроллим к правильной позиции
window.scrollTo(0, targetPosition);

// Возвращаем хеш в URL
setTimeout(() => {
history.replaceState(null, null, hash);
}, 100);
}
}
}

document.addEventListener('DOMContentLoaded', function() {
setTimeout(preventBrowserAnchorScroll, 50);
});
window.addEventListener('hashchange', preventBrowserAnchorScroll);
</script>
```

3. CSS решение для мгновенного применения

Добавьте в CSS (если еще нет):

```css
/* Отключаем плавный скролл для якорных переходов */
html {
scroll-behavior: auto;
}

/* Скрываем элемент до применения скрипта */
[data-anchor-target] {
opacity: 0;
}
```

И обновите скрипт:

```html
<script>
function smoothAnchorScroll() {
if (window.location.hash) {
const hash = decodeURIComponent(window.location.hash.substring(1));
const element = document.getElementById(hash);

if (element) {
// Помечаем элемент для CSS
element.setAttribute('data-anchor-target', 'true');

const header = document.querySelector('header');
const headerHeight = header ? header.offsetHeight : 80;
const targetPosition = Math.max(0, element.offsetTop - headerHeight);

// Мгновенный скролл
window.scrollTo(0, targetPosition);

// Показываем элемент после скролла
setTimeout(() => {
element.removeAttribute('data-anchor-target');
}, 100);
}
}
}

document.addEventListener('DOMContentLoaded', function() {
setTimeout(smoothAnchorScroll, 10); // Минимальная задержка
});
window.addEventListener('hashchange', smoothAnchorScroll);
</script>
```

4. Оптимальный вариант (рекомендую)

```html
<script>
function optimalAnchorScroll() {
if (window.location.hash) {
const hash = window.location.hash;
const decodedHash = decodeURIComponent(hash.substring(1));
const element = document.getElementById(decodedHash);

if (element) {
// Быстрый расчет и скролл
const header = document.querySelector('header');
const headerHeight = header ? header.offsetHeight : 80;
const targetScroll = Math.max(0, element.offsetTop - headerHeight - 5);

// Один точный скролл вместо нескольких
requestAnimationFrame(() => {
window.scrollTo(0, targetScroll);
});
}
}
}

// Запускаем как можно раньше
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', optimalAnchorScroll);
} else {
optimalAnchorScroll();
}

window.addEventListener('hashchange', optimalAnchorScroll);
</script>
```

Рекомендую вариант 4 - он использует requestAnimationFrame для максимально быстрого и плавного скролла без видимого "дергания".

Если все равно заметно мигание, попробуйте вариант 2 - он полностью предотвращает браузерный скролл.

Какой вариант больше подходит?
Зззз
Прикрепления:
66666.zip (17.2 Kb)
55555
Прикрепления:
shhshhshhshhsh.zip (17.6 Kb)
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Статистика посещений сайта</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}

body {
background-color: #f5f7fa;
color: #333;
line-height: 1.6;
padding: 20px;
}

.container {
max-width: 1400px;
margin: 0 auto;
background-color: white;
border-radius: 10px;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
padding: 25px;
}

header {
text-align: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 1px solid #eaeaea;
}

h1 {
color: #2c3e50;
margin-bottom: 10px;
}

.description {
color: #7f8c8d;
max-width: 800px;
margin: 0 auto;
}

.upload-section {
background-color: #f8f9fa;
padding: 20px;
border-radius: 8px;
margin-bottom: 30px;
border: 1px dashed #dee2e6;
}

.upload-label {
display: block;
margin-bottom: 15px;
font-weight: 600;
color: #495057;
}

.path-input {
width: 100%;
padding: 12px 15px;
border: 1px solid #ced4da;
border-radius: 4px;
font-size: 16px;
margin-bottom: 15px;
}

.btn {
background-color: #3498db;
color: white;
border: none;
padding: 12px 25px;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
font-weight: 600;
transition: background-color 0.3s;
}

.btn:hover {
background-color: #2980b9;
}

.btn-secondary {
background-color: #6c757d;
}

.btn-secondary:hover {
background-color: #5a6268;
}

.file-input-wrapper {
position: relative;
display: inline-block;
margin-right: 10px;
}

.file-input {
position: absolute;
left: 0;
top: 0;
opacity: 0;
width: 100%;
height: 100%;
cursor: pointer;
}

.filters {
display: flex;
flex-wrap: wrap;
gap: 15px;
margin-bottom: 25px;
padding: 15px;
background-color: #f8f9fa;
border-radius: 8px;
}

.filter-group {
flex: 1;
min-width: 200px;
}

.filter-label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #495057;
}

.filter-input, .filter-select {
width: 100%;
padding: 10px;
border: 1px solid #ced4da;
border-radius: 4px;
}

.stats-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 25px;
margin-bottom: 30px;
}

.stats-card {
background-color: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}

.stats-card h3 {
margin-bottom: 15px;
color: #2c3e50;
border-bottom: 1px solid #eaeaea;
padding-bottom: 10px;
}

.chart-container {
height: 300px;
margin-top: 10px;
}

.popular-pages-list {
margin-top: 15px;
max-height: 250px;
overflow-y: auto;
}

.page-item {
display: flex;
justify-content: space-between;
padding: 8px 10px;
border-bottom: 1px solid #f0f0f0;
}

.page-item:hover {
background-color: #f8f9fa;
}

.table-container {
overflow-x: auto;
margin-top: 30px;
}

table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
}

th, td {
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid #eaeaea;
}

th {
background-color: #f8f9fa;
font-weight: 600;
color: #495057;
}

tr:hover {
background-color: #f8f9fa;
}

.loading {
text-align: center;
padding: 20px;
display: none;
}

.error {
color: #e74c3c;
padding: 10px;
background-color: #fadbd8;
border-radius: 4px;
margin-top: 10px;
display: none;
}

.calendar-container {
margin-top: 30px;
padding: 20px;
background-color: #f8f9fa;
border-radius: 8px;
}

.calendar-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}

.calendar-title {
font-weight: 600;
color: #495057;
font-size: 18px;
}

.calendar-nav {
display: flex;
gap: 10px;
}

.calendar-nav-btn {
background-color: #3498db;
color: white;
border: none;
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
font-weight: 600;
}

.calendar {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 5px;
}

.calendar-day {
padding: 8px;
text-align: center;
border-radius: 4px;
background-color: #e9ecef;
cursor: pointer;
transition: all 0.2s;
}

.calendar-day.has-data {
background-color: #3498db;
color: white;
font-weight: bold;
}

.calendar-day.has-data:hover {
background-color: #2980b9;
transform: scale(1.05);
}

.calendar-day.selected {
background-color: #e74c3c;
color: white;
}

.calendar-day.other-month {
background-color: #f8f9fa;
color: #adb5bd;
}

.calendar-day-header {
padding: 8px;
text-align: center;
font-weight: bold;
background-color: #dee2e6;
}

.period-selector {
display: flex;
gap: 10px;
margin-bottom: 15px;
}

.file-info {
margin-top: 10px;
padding: 10px;
background-color: #e9ecef;
border-radius: 4px;
font-size: 14px;
}

@media (max-width: 768px) {
.stats-container {
grid-template-columns: 1fr;
}

.filters {
flex-direction: column;
}

.period-selector {
flex-direction: column;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>Статистика посещений сайта</h1>
<p class="description">Загрузите логи IIS 10.0 для анализа посещений. Система автоматически отфильтрует запросы к JS, CSS и другим ресурсам, оставив только основные страницы.</p>
</header>

<section class="upload-section">
<label class="upload-label">Выберите файлы логов или папку:</label>
<div class="file-input-wrapper">
<button class="btn">Выбрать файлы</button>
<input type="file" id="logFiles" class="file-input" multiple accept=".log,.txt">
</div>
<div class="file-input-wrapper">
<button class="btn btn-secondary">Выбрать папку</button>
<input type="file" id="logFolder" class="file-input" webkitdirectory directory multiple>
</div>
<button id="loadLogs" class="btn">Загрузить логи</button>
<button id="clearCache" class="btn btn-secondary">Очистить кеш</button>
<div id="fileInfo" class="file-info"></div>
<div id="loading" class="loading">Загрузка и анализ логов...</div>
<div id="error" class="error"></div>
</section>

<section class="filters">
<div class="filter-group">
<label class="filter-label" for="dateFrom">Период с:</label>
<input type="date" id="dateFrom" class="filter-input">
</div>
<div class="filter-group">
<label class="filter-label" for="dateTo">Период по:</label>
<input type="date" id="dateTo" class="filter-input">
</div>
<div class="filter-group">
<label class="filter-label" for="userFilter">Фильтр по пользователю:</label>
<input type="text" id="userFilter" class="filter-input" placeholder="Введите имя пользователя">
</div>
<div class="filter-group">
<label class="filter-label" for="pageFilter">Фильтр по странице:</label>
<input type="text" id="pageFilter" class="filter-input" placeholder="Введите часть URL">
</div>
</section>

<div class="period-selector">
<div class="filter-group">
<label class="filter-label" for="groupBy">Группировать по:</label>
<select id="groupBy" class="filter-select">
<option value="day">Дням</option>
<option value="month">Месяцам</option>
<option value="year">Годам</option>
</select>
</div>
</div>

<div class="stats-container">
<div class="stats-card">
<h3>Посещения по датам</h3>
<div class="chart-container">
<canvas id="visitsByDateChart"></canvas>
</div>
</div>
<div class="stats-card">
<h3>Топ посещаемых страниц</h3>
<div class="chart-container">
<canvas id="popularPagesChart"></canvas>
</div>
<div class="popular-pages-list" id="popularPagesList">
<!-- Список популярных страниц будет здесь -->
</div>
</div>
</div>

<div class="calendar-container">
<div class="calendar-header">
<button id="prevMonth" class="calendar-nav-btn">←</button>
<h3 class="calendar-title" id="calendarTitle">Октябрь 2025</h3>
<button id="nextMonth" class="calendar-nav-btn">→</button>
</div>
<div class="calendar" id="calendar">
<!-- Календарь будет сгенерирован здесь -->
</div>
</div>

<div class="table-container">
<h3>Детальная статистика посещений</h3>
<table id="statsTable">
<thead>
<tr>
<th>Дата</th>
<th>Время</th>
<th>Пользователь</th>
<th>IP-адрес</th>
<th>Метод</th>
<th>Страница</th>
<th>Код ответа</th>
</tr>
</thead>
<tbody id="statsTableBody">
<!-- Данные будут загружены здесь -->
</tbody>
</table>
</div>
</div>

<script>
document.addEventListener('DOMContentLoaded', function() {
const loadLogsBtn = document.getElementById('loadLogs');
const logFilesInput = document.getElementById('logFiles');
const logFolderInput = document.getElementById('logFolder');
const clearCacheBtn = document.getElementById('clearCache');
const loadingDiv = document.getElementById('loading');
const errorDiv = document.getElementById('error');
const fileInfoDiv = document.getElementById('fileInfo');
const statsTableBody = document.getElementById('statsTableBody');
const dateFrom = document.getElementById('dateFrom');
const dateTo = document.getElementById('dateTo');
const userFilter = document.getElementById('userFilter');
const pageFilter = document.getElementById('pageFilter');
const groupBy = document.getElementById('groupBy');
const popularPagesList = document.getElementById('popularPagesList');
const calendar = document.getElementById('calendar');
const calendarTitle = document.getElementById('calendarTitle');
const prevMonthBtn = document.getElementById('prevMonth');
const nextMonthBtn = document.getElementById('nextMonth');

let allLogs = [];
let filteredLogs = [];
let dateRange = {};
let currentCalendarDate = new Date();
let selectedDate = null;
let selectedFiles = [];

// Инициализация графиков
const visitsByDateChart = new Chart(
document.getElementById('visitsByDateChart'),
{
type: 'bar',
data: {
labels: [],
datasets: [{
label: 'Количество посещений',
data: [],
backgroundColor: 'rgba(54, 162, 235, 0.5)',
borderColor: 'rgba(54, 162, 235, 1)',
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true
}
}
}
}
);

const popularPagesChart = new Chart(
document.getElementById('popularPagesChart'),
{
type: 'pie',
data: {
labels: [],
datasets: [{
data: [],
backgroundColor: [
'rgba(255, 99, 132, 0.5)',
'rgba(54, 162, 235, 0.5)',
'rgba(255, 206, 86, 0.5)',
'rgba(75, 192, 192, 0.5)',
'rgba(153, 102, 255, 0.5)',
'rgba(255, 159, 64, 0.5)',
'rgba(199, 199, 199, 0.5)'
],
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'right',
}
}
}
}
);

// Загрузка кешированных данных
function loadCachedData() {
const cachedLogs = localStorage.getItem('iisLogsData');
const cachedPath = localStorage.getItem('iisLogsPath');

if (cachedLogs && cachedPath) {
allLogs = JSON.parse(cachedLogs);
dateRange = getDateRange(allLogs);
setDateFilters(dateRange);
currentCalendarDate = new Date(dateRange.max);
updateCalendar();
applyFilters();

fileInfoDiv.textContent = `Загружены кешированные данные из: ${cachedPath}`;
}
}

// Сохранение данных в кеш
function saveToCache(data, path) {
localStorage.setItem('iisLogsData', JSON.stringify(data));
localStorage.setItem('iisLogsPath', path);
}

// Очистка кеша
clearCacheBtn.addEventListener('click', function() {
localStorage.removeItem('iisLogsData');
localStorage.removeItem('iisLogsPath');
allLogs = [];
filteredLogs = [];
statsTableBody.innerHTML = '';
fileInfoDiv.textContent = '';
visitsByDateChart.data.labels = [];
visitsByDateChart.data.datasets[0].data = [];
visitsByDateChart.update();
popularPagesChart.data.labels = [];
popularPagesChart.data.datasets[0].data = [];
popularPagesChart.update();
popularPagesList.innerHTML = '';
calendar.innerHTML = '';
alert('Кеш очищен');
});

// Обработка выбора файлов
logFilesInput.addEventListener('change', function(e) {
selectedFiles = Array.from(e.target.files);
updateFileInfo();
});

// Обработка выбора папки
logFolderInput.addEventListener('change', function(e) {
selectedFiles = Array.from(e.target.files);
updateFileInfo();
});

// Обновление информации о выбранных файлах
function updateFileInfo() {
if (selectedFiles.length > 0) {
const fileNames = selectedFiles.map(f => f.name).join(', ');
fileInfoDiv.textContent = `Выбрано файлов: ${selectedFiles.length}. ${fileNames}`;
} else {
fileInfoDiv.textContent = '';
}
}

// Загрузка логов
loadLogsBtn.addEventListener('click', function() {
if (selectedFiles.length === 0) {
showError('Пожалуйста, выберите файлы логов или папку');
return;
}

loadingDiv.style.display = 'block';
errorDiv.style.display = 'none';

// Чтение файлов
const fileReaders = [];

selectedFiles.forEach(file => {
const reader = new FileReader();
fileReaders.push(new Promise((resolve) => {
reader.onload = function(e) {
resolve({
name: file.name,
content: e.target.result
});
};
reader.readAsText(file);
}));
});

Promise.all(fileReaders)
.then(fileContents => {
try {
// Парсинг логов из всех файлов
allLogs = [];

fileContents.forEach(file => {
const logs = parseLogFile(file.content, file.name);
allLogs = allLogs.concat(logs);
});

if (allLogs.length === 0) {
throw new Error('Не удалось распознать данные в логах');
}

// Сохраняем в кеш
const path = selectedFiles.length === 1 ?
selectedFiles[0].name :
`Папка с ${selectedFiles.length} файлами`;
saveToCache(allLogs, path);

dateRange = getDateRange(allLogs);
setDateFilters(dateRange);
currentCalendarDate = new Date(dateRange.max);
updateCalendar();
applyFilters();
loadingDiv.style.display = 'none';
} catch (error) {
loadingDiv.style.display = 'none';
showError('Ошибка при обработке логов: ' + error.message);
}
})
.catch(error => {
loadingDiv.style.display = 'none';
showError('Ошибка при чтении файлов: ' + error.message);
});
});

// Парсинг файла логов
function parseLogFile(content, filename) {
const lines = content.split('\n');
const logs = [];

// Регулярное выражение для парсинга строки лога IIS
// Формат: дата время s-ip cs-method cs-uri-stem cs-uri-query s-port cs-username c-ip cs(User-Agent) sc-status sc-substatus sc-win32-status time-taken
const logRegex = /^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2}:\d{2})\s+([\d.]+)\s+(\w+)\s+([^\s?]+)(?:\?([^\s]+))?\s+(\d+)\s+([^\s]+)\s+([\d.]+)\s+([^\s]+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/;

for (const line of lines) {
if (!line.trim()) continue;

const match = line.match(logRegex);
if (match) {
const [
fullMatch,
date,
time,
serverIp,
method,
uriStem,
uriQuery,
port,
username,
clientIp,
userAgent,
statusCode,
subStatus,
win32Status,
timeTaken
] = match;

// Фильтруем только успешные запросы и основные страницы
if (statusCode === '200' &&
!uriStem.match(/\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$/i) &&
uriStem !== '/') {

logs.push({
date: date,
time: time,
user: username !== '-' ? username : 'Неизвестно',
ip: clientIp,
method: method,
page: uriStem + (uriQuery ? '?' + uriQuery : ''),
statusCode: parseInt(statusCode),
filename: filename
});
}
}
}

return logs;
}

// Применение фильтров
[dateFrom, dateTo, userFilter, pageFilter, groupBy].forEach(filter => {
filter.addEventListener('change', applyFilters);
});

userFilter.addEventListener('input', applyFilters);
pageFilter.addEventListener('input', applyFilters);

// Навигация по календарю
prevMonthBtn.addEventListener('click', function() {
currentCalendarDate.setMonth(currentCalendarDate.getMonth() - 1);
updateCalendar();
});

nextMonthBtn.addEventListener('click', function() {
currentCalendarDate.setMonth(currentCalendarDate.getMonth() + 1);
updateCalendar();
});

function applyFilters() {
filteredLogs = allLogs.filter(log => {
// Фильтр по дате
const logDate = new Date(log.date);
const fromDate = dateFrom.value ? new Date(dateFrom.value) : new Date(dateRange.min);
const toDate = dateTo.value ? new Date(dateTo.value) : new Date(dateRange.max);

const dateMatch = logDate >= fromDate && logDate <= toDate;

// Фильтр по пользователю
const userMatch = !userFilter.value ||
log.user.toLowerCase().includes(userFilter.value.toLowerCase());

// Фильтр по странице
const pageMatch = !pageFilter.value ||
log.page.toLowerCase().includes(pageFilter.value.toLowerCase());

return dateMatch && userMatch && pageMatch;
});

updateStatsTable();
updateCharts();
updatePopularPagesList();
}

function updateStatsTable() {
statsTableBody.innerHTML = '';

filteredLogs.forEach(log => {
const row = document.createElement('tr');

const dateCell = document.createElement('td');
dateCell.textContent = log.date;
row.appendChild(dateCell);

const timeCell = document.createElement('td');
timeCell.textContent = log.time;
row.appendChild(timeCell);

const userCell = document.createElement('td');
userCell.textContent = log.user;
row.appendChild(userCell);

const ipCell = document.createElement('td');
ipCell.textContent = log.ip;
row.appendChild(ipCell);

const methodCell = document.createElement('td');
methodCell.textContent = log.method;
row.appendChild(methodCell);

const pageCell = document.createElement('td');
pageCell.textContent = log.page;
row.appendChild(pageCell);

const codeCell = document.createElement('td');
codeCell.textContent = log.statusCode;
row.appendChild(codeCell);

statsTableBody.appendChild(row);
});
}

function updateCharts() {
// Обновление графика по датам с группировкой
const visitsByPeriod = {};
filteredLogs.forEach(log => {
let periodKey;
const date = new Date(log.date);

switch(groupBy.value) {
case 'month':
periodKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
break;
case 'year':
periodKey = `${date.getFullYear()}`;
break;
default: // day
periodKey = log.date;
}

visitsByPeriod[periodKey] = (visitsByPeriod[periodKey] || 0) + 1;
});

const periods = Object.keys(visitsByPeriod).sort();
const visits = periods.map(period => visitsByPeriod[period]);

visitsByDateChart.data.labels = periods;
visitsByDateChart.data.datasets[0].data = visits;
visitsByDateChart.update();

// Обновление графика популярных страниц
const pageCounts = {};
filteredLogs.forEach(log => {
pageCounts[log.page] = (pageCounts[log.page] || 0) + 1;
});

const sortedPages = Object.entries(pageCounts)
.sort((a, b) => b[1] - a[1])
.slice(0, 7);

popularPagesChart.data.labels = sortedPages.map(item => {
// Сокращаем длинные названия страниц для легенды
return item[0].length > 30 ? item[0].substring(0, 30) + '...' : item[0];
});
popularPagesChart.data.datasets[0].data = sortedPages.map(item => item[1]);
popularPagesChart.update();
}

function updatePopularPagesList() {
const pageCounts = {};
filteredLogs.forEach(log => {
pageCounts[log.page] = (pageCounts[log.page] || 0) + 1;
});

const sortedPages = Object.entries(pageCounts)
.sort((a, b) => b[1] - a[1])
.slice(0, 10);

popularPagesList.innerHTML = '';

sortedPages.forEach(([page, count]) => {
const pageItem = document.createElement('div');
pageItem.className = 'page-item';

const pageName = document.createElement('span');
pageName.textContent = page;

const pageCount = document.createElement('span');
pageCount.textContent = count;
pageCount.style.fontWeight = 'bold';
pageCount.style.color = '#3498db';

pageItem.appendChild(pageName);
pageItem.appendChild(pageCount);
popularPagesList.appendChild(pageItem);
});
}

function updateCalendar() {
// Обновляем заголовок календаря
const monthNames = ["Январь", "Февраль", "Март", "Апрель", "Май", "Июнь",
"Июль", "Август", "Сентябрь", "Октябрь", "Ноябрь", "Декабрь"];
calendarTitle.textContent = `${monthNames[currentCalendarDate.getMonth()]} ${currentCalendarDate.getFullYear()}`;

// Получаем уникальные даты с данными
const datesWithData = [...new Set(allLogs.map(log => log.date))].sort();

// Создаем объект для быстрой проверки наличия данных по дате
const dataDates = {};
datesWithData.forEach(date => {
dataDates[date] = true;
});

// Очищаем календарь
calendar.innerHTML = '';

// Добавляем заголовки дней недели
const daysOfWeek = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'];
daysOfWeek.forEach(day => {
const dayHeader = document.createElement('div');
dayHeader.className = 'calendar-day-header';
dayHeader.textContent = day;
calendar.appendChild(dayHeader);
});

// Определяем первый и последний день месяца для отображения
const firstDayOfMonth = new Date(currentCalendarDate.getFullYear(), currentCalendarDate.getMonth(), 1);
const lastDayOfMonth = new Date(currentCalendarDate.getFullYear(), currentCalendarDate.getMonth() + 1, 0);

// Определяем день недели первого дня месяца (0 - воскресенье, 1 - понедельник и т.д.)
let firstDayOfWeek = firstDayOfMonth.getDay();
// Преобразуем к формату Пн=0, Вт=1, ..., Вс=6
firstDayOfWeek = firstDayOfWeek === 0 ? 6 : firstDayOfWeek - 1;

// Добавляем пустые ячейки для дней предыдущего месяца
for (let i = 0; i < firstDayOfWeek; i++) {
const emptyDay = document.createElement('div');
emptyDay.className = 'calendar-day other-month';
calendar.appendChild(emptyDay);
}

// Заполняем календарь днями текущего месяца
for (let day = 1; day <= lastDayOfMonth.getDate(); day++) {
const dayElement = document.createElement('div');
dayElement.className = 'calendar-day';

const dateString = `${currentCalendarDate.getFullYear()}-${String(currentCalendarDate.getMonth() + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
dayElement.textContent = day;
dayElement.dataset.date = dateString;

if (dataDates[dateString]) {
dayElement.classList.add('has-data');
dayElement.title = `Есть данные за ${dateString}`;

// Добавляем обработчик клика
dayElement.addEventListener('click', function() {
// Снимаем выделение с предыдущей выбранной даты
if (selectedDate) {
const prevSelected = document.querySelector(`.calendar-day[data-date="${selectedDate}"]`);
if (prevSelected) {
prevSelected.classList.remove('selected');
}
}

// Выделяем новую дату
dayElement.classList.add('selected');
selectedDate = dateString;

// Устанавливаем фильтр на выбранную дату
dateFrom.value = dateString;
dateTo.value = dateString;
applyFilters();
});
}

calendar.appendChild(dayElement);
}
}

function getDateRange(logs) {
const dates = logs.map(log => new Date(log.date));
const minDate = new Date(Math.min(...dates));
const maxDate = new Date(Math.max(...dates));

return {
min: minDate.toISOString().split('T')[0],
max: maxDate.toISOString().split('T')[0]
};
}

function setDateFilters(dateRange) {
dateFrom.value = dateRange.min;
dateTo.value = dateRange.max;
}

function showError(message) {
errorDiv.textContent = message;
errorDiv.style.display = 'block';
}

// Загружаем кешированные данные при запуске
loadCachedData();
});
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Статистика посещений сайта</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}

body {
background-color: #f5f7fa;
color: #333;
line-height: 1.6;
padding: 20px;
}

.container {
max-width: 1400px;
margin: 0 auto;
background-color: white;
border-radius: 10px;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
padding: 25px;
}

header {
text-align: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 1px solid #eaeaea;
}

h1 {
color: #2c3e50;
margin-bottom: 10px;
}

.description {
color: #7f8c8d;
max-width: 800px;
margin: 0 auto;
}

.upload-section {
background-color: #f8f9fa;
padding: 20px;
border-radius: 8px;
margin-bottom: 30px;
border: 1px dashed #dee2e6;
}

.upload-label {
display: block;
margin-bottom: 15px;
font-weight: 600;
color: #495057;
}

.path-input {
width: 100%;
padding: 12px 15px;
border: 1px solid #ced4da;
border-radius: 4px;
font-size: 16px;
margin-bottom: 15px;
}

.btn {
background-color: #3498db;
color: white;
border: none;
padding: 12px 25px;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
font-weight: 600;
transition: background-color 0.3s;
}

.btn:hover {
background-color: #2980b9;
}

.btn-secondary {
background-color: #6c757d;
}

.btn-secondary:hover {
background-color: #5a6268;
}

.file-input-wrapper {
position: relative;
display: inline-block;
margin-right: 10px;
}

.file-input {
position: absolute;
left: 0;
top: 0;
opacity: 0;
width: 100%;
height: 100%;
cursor: pointer;
}

.filters {
display: flex;
flex-wrap: wrap;
gap: 15px;
margin-bottom: 25px;
padding: 15px;
background-color: #f8f9fa;
border-radius: 8px;
}

.filter-group {
flex: 1;
min-width: 200px;
}

.filter-label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #495057;
}

.filter-input, .filter-select {
width: 100%;
padding: 10px;
border: 1px solid #ced4da;
border-radius: 4px;
}

.stats-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 25px;
margin-bottom: 30px;
}

.stats-card {
background-color: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}

.stats-card h3 {
margin-bottom: 15px;
color: #2c3e50;
border-bottom: 1px solid #eaeaea;
padding-bottom: 10px;
}

.chart-container {
height: 300px;
margin-top: 10px;
}

.popular-pages-list {
margin-top: 15px;
max-height: 250px;
overflow-y: auto;
}

.page-item {
display: flex;
justify-content: space-between;
padding: 8px 10px;
border-bottom: 1px solid #f0f0f0;
}

.page-item:hover {
background-color: #f8f9fa;
}

.table-container {
overflow-x: auto;
margin-top: 30px;
}

table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
}

th, td {
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid #eaeaea;
}

th {
background-color: #f8f9fa;
font-weight: 600;
color: #495057;
}

tr:hover {
background-color: #f8f9fa;
}

.loading {
text-align: center;
padding: 20px;
display: none;
}

.error {
color: #e74c3c;
padding: 10px;
background-color: #fadbd8;
border-radius: 4px;
margin-top: 10px;
display: none;
}

.calendar-container {
margin-top: 30px;
padding: 20px;
background-color: #f8f9fa;
border-radius: 8px;
}

.calendar-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}

.calendar-title {
font-weight: 600;
color: #495057;
font-size: 18px;
}

.calendar-nav {
display: flex;
gap: 10px;
}

.calendar-nav-btn {
background-color: #3498db;
color: white;
border: none;
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
font-weight: 600;
}

.calendar {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 5px;
}

.calendar-day {
padding: 8px;
text-align: center;
border-radius: 4px;
background-color: #e9ecef;
cursor: pointer;
transition: all 0.2s;
}

.calendar-day.has-data {
background-color: #3498db;
color: white;
font-weight: bold;
}

.calendar-day.has-data:hover {
background-color: #2980b9;
transform: scale(1.05);
}

.calendar-day.selected {
background-color: #e74c3c;
color: white;
}

.calendar-day.other-month {
background-color: #f8f9fa;
color: #adb5bd;
}

.calendar-day-header {
padding: 8px;
text-align: center;
font-weight: bold;
background-color: #dee2e6;
}

.period-selector {
display: flex;
gap: 10px;
margin-bottom: 15px;
}

.file-info {
margin-top: 10px;
padding: 10px;
background-color: #e9ecef;
border-radius: 4px;
font-size: 14px;
}

@media (max-width: 768px) {
.stats-container {
grid-template-columns: 1fr;
}

.filters {
flex-direction: column;
}

.period-selector {
flex-direction: column;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>Статистика посещений сайта</h1>
<p class="description">Загрузите логи IIS 10.0 для анализа посещений. Система автоматически отфильтрует запросы к JS, CSS и другим ресурсам, оставив только основные страницы.</p>
</header>

<section class="upload-section">
<label class="upload-label">Выберите файлы логов или папку:</label>
<div class="file-input-wrapper">
<button class="btn">Выбрать файлы</button>
<input type="file" id="logFiles" class="file-input" multiple accept=".log,.txt">
</div>
<div class="file-input-wrapper">
<button class="btn btn-secondary">Выбрать папку</button>
<input type="file" id="logFolder" class="file-input" webkitdirectory directory multiple>
</div>
<button id="loadLogs" class="btn">Загрузить логи</button>
<button id="clearCache" class="btn btn-secondary">Очистить кеш</button>
<div id="fileInfo" class="file-info"></div>
<div id="loading" class="loading">Загрузка и анализ логов...</div>
<div id="error" class="error"></div>
</section>

<section class="filters">
<div class="filter-group">
<label class="filter-label" for="dateFrom">Период с:</label>
<input type="date" id="dateFrom" class="filter-input">
</div>
<div class="filter-group">
<label class="filter-label" for="dateTo">Период по:</label>
<input type="date" id="dateTo" class="filter-input">
</div>
<div class="filter-group">
<label class="filter-label" for="userFilter">Фильтр по пользователю:</label>
<input type="text" id="userFilter" class="filter-input" placeholder="Введите имя пользователя">
</div>
<div class="filter-group">
<label class="filter-label" for="pageFilter">Фильтр по странице:</label>
<input type="text" id="pageFilter" class="filter-input" placeholder="Введите часть URL">
</div>
</section>

<div class="period-selector">
<div class="filter-group">
<label class="filter-label" for="groupBy">Группировать по:</label>
<select id="groupBy" class="filter-select">
<option value="day">Дням</option>
<option value="month">Месяцам</option>
<option value="year">Годам</option>
</select>
</div>
</div>

<div class="stats-container">
<div class="stats-card">
<h3>Посещения по датам</h3>
<div class="chart-container">
<canvas id="visitsByDateChart"></canvas>
</div>
</div>
<div class="stats-card">
<h3>Топ посещаемых страниц</h3>
<div class="chart-container">
<canvas id="popularPagesChart"></canvas>
</div>
<div class="popular-pages-list" id="popularPagesList">
<!-- Список популярных страниц будет здесь -->
</div>
</div>
</div>

<div class="calendar-container">
<div class="calendar-header">
<button id="prevMonth" class="calendar-nav-btn">←</button>
<h3 class="calendar-title" id="calendarTitle">Октябрь 2025</h3>
<button id="nextMonth" class="calendar-nav-btn">→</button>
</div>
<div class="calendar" id="calendar">
<!-- Календарь будет сгенерирован здесь -->
</div>
</div>

<div class="table-container">
<h3>Детальная статистика посещений</h3>
<table id="statsTable">
<thead>
<tr>
<th>Дата</th>
<th>Время</th>
<th>Пользователь</th>
<th>IP-адрес</th>
<th>Метод</th>
<th>Страница</th>
<th>Код ответа</th>
</tr>
</thead>
<tbody id="statsTableBody">
<!-- Данные будут загружены здесь -->
</tbody>
</table>
</div>
</div>

<script>
document.addEventListener('DOMContentLoaded', function() {
const loadLogsBtn = document.getElementById('loadLogs');
const logFilesInput = document.getElementById('logFiles');
const logFolderInput = document.getElementById('logFolder');
const clearCacheBtn = document.getElementById('clearCache');
const loadingDiv = document.getElementById('loading');
const errorDiv = document.getElementById('error');
const fileInfoDiv = document.getElementById('fileInfo');
const statsTableBody = document.getElementById('statsTableBody');
const dateFrom = document.getElementById('dateFrom');
const dateTo = document.getElementById('dateTo');
const userFilter = document.getElementById('userFilter');
const pageFilter = document.getElementById('pageFilter');
const groupBy = document.getElementById('groupBy');
const popularPagesList = document.getElementById('popularPagesList');
const calendar = document.getElementById('calendar');
const calendarTitle = document.getElementById('calendarTitle');
const prevMonthBtn = document.getElementById('prevMonth');
const nextMonthBtn = document.getElementById('nextMonth');

let allLogs = [];
let filteredLogs = [];
let dateRange = {};
let currentCalendarDate = new Date();
let selectedDate = null;
let selectedFiles = [];

// Инициализация графиков
const visitsByDateChart = new Chart(
document.getElementById('visitsByDateChart'),
{
type: 'bar',
data: {
labels: [],
datasets: [{
label: 'Количество посещений',
data: [],
backgroundColor: 'rgba(54, 162, 235, 0.5)',
borderColor: 'rgba(54, 162, 235, 1)',
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true
}
}
}
}
);

const popularPagesChart = new Chart(
document.getElementById('popularPagesChart'),
{
type: 'pie',
data: {
labels: [],
datasets: [{
data: [],
backgroundColor: [
'rgba(255, 99, 132, 0.5)',
'rgba(54, 162, 235, 0.5)',
'rgba(255, 206, 86, 0.5)',
'rgba(75, 192, 192, 0.5)',
'rgba(153, 102, 255, 0.5)',
'rgba(255, 159, 64, 0.5)',
'rgba(199, 199, 199, 0.5)'
],
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'right',
}
}
}
}
);

// Загрузка кешированных данных
function loadCachedData() {
const cachedLogs = localStorage.getItem('iisLogsData');
const cachedPath = localStorage.getItem('iisLogsPath');

if (cachedLogs && cachedPath) {
allLogs = JSON.parse(cachedLogs);
dateRange = getDateRange(allLogs);
setDateFilters(dateRange);
currentCalendarDate = new Date(dateRange.max);
updateCalendar();
applyFilters();

fileInfoDiv.textContent = `Загружены кешированные данные из: ${cachedPath}`;
}
}

// Сохранение данных в кеш
function saveToCache(data, path) {
localStorage.setItem('iisLogsData', JSON.stringify(data));
localStorage.setItem('iisLogsPath', path);
}

// Очистка кеша
clearCacheBtn.addEventListener('click', function() {
localStorage.removeItem('iisLogsData');
localStorage.removeItem('iisLogsPath');
allLogs = [];
filteredLogs = [];
statsTableBody.innerHTML = '';
fileInfoDiv.textContent = '';
visitsByDateChart.data.labels = [];
visitsByDateChart.data.datasets[0].data = [];
visitsByDateChart.update();
popularPagesChart.data.labels = [];
popularPagesChart.data.datasets[0].data = [];
popularPagesChart.update();
popularPagesList.innerHTML = '';
calendar.innerHTML = '';
alert('Кеш очищен');
});

// Обработка выбора файлов
logFilesInput.addEventListener('change', function(e) {
selectedFiles = Array.from(e.target.files);
updateFileInfo();
});

// Обработка выбора папки
logFolderInput.addEventListener('change', function(e) {
selectedFiles = Array.from(e.target.files);
updateFileInfo();
});

// Обновление информации о выбранных файлах
function updateFileInfo() {
if (selectedFiles.length > 0) {
fileInfoDiv.textContent = `Выбрано файлов: ${selectedFiles.length}`;
} else {
fileInfoDiv.textContent = '';
}
}

// Загрузка логов
loadLogsBtn.addEventListener('click', function() {
if (selectedFiles.length === 0) {
showError('Пожалуйста, выберите файлы логов или папку');
return;
}

loadingDiv.style.display = 'block';
errorDiv.style.display = 'none';

// Чтение файлов
const fileReaders = [];

selectedFiles.forEach(file => {
const reader = new FileReader();
fileReaders.push(new Promise((resolve) => {
reader.onload = function(e) {
resolve({
name: file.name,
content: e.target.result
});
};
reader.readAsText(file);
}));
});

Promise.all(fileReaders)
.then(fileContents => {
try {
// Парсинг логов из всех файлов
allLogs = [];

fileContents.forEach(file => {
const logs = parseLogFile(file.content, file.name);
allLogs = allLogs.concat(logs);
});

if (allLogs.length === 0) {
throw new Error('Не удалось распознать данные в логах. Проверьте формат файлов.');
}

// Сохраняем в кеш
const path = selectedFiles.length === 1 ?
selectedFiles[0].name :
`Папка с ${selectedFiles.length} файлами`;
saveToCache(allLogs, path);

dateRange = getDateRange(allLogs);
setDateFilters(dateRange);
currentCalendarDate = new Date(dateRange.max);
updateCalendar();
applyFilters();
loadingDiv.style.display = 'none';
} catch (error) {
loadingDiv.style.display = 'none';
showError('Ошибка при обработке логов: ' + error.message);
}
})
.catch(error => {
loadingDiv.style.display = 'none';
showError('Ошибка при чтении файлов: ' + error.message);
});
});

// Парсинг файла логов
function parseLogFile(content, filename) {
const lines = content.split('\n');
const logs = [];

// Регулярное выражение для парсинга строки лога IIS
// Формат: дата время s-ip cs-method cs-uri-stem cs-uri-query s-port cs-username c-ip cs(User-Agent) sc-status sc-substatus sc-win32-status time-taken
// Пример: 2025-10-27 08:19:27 10.64.241.57 POST /22221123/Reportsu.aspx report=Catalog_Point 80 MYPC\IvanovAA 15.34.19.44 Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+Yowser/2.5+Safari/537.36 200 0 0 4232
const logRegex = /^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2}:\d{2})\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/;

for (const line of lines) {
if (!line.trim()) continue;

const match = line.match(logRegex);
if (match) {
const [
fullMatch,
date,
time,
serverIp,
method,
uriStem,
uriQuery,
port,
username,
clientIp,
userAgent,
statusCode,
subStatus,
win32Status,
timeTaken
] = match;

// Фильтруем только успешные запросы и основные страницы
// Исключаем статические ресурсы (JS, CSS, изображения и т.д.)
if (statusCode === '200' &&
!uriStem.match(/\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$/i) &&
uriStem !== '/favicon.ico' &&
uriStem !== '/') {

logs.push({
date: date,
time: time,
user: username !== '-' ? username : 'Неизвестно',
ip: clientIp,
method: method,
page: uriStem + (uriQuery && uriQuery !== '-' ? '?' + uriQuery : ''),
statusCode: parseInt(statusCode),
filename: filename
});
}
}
}

return logs;
}

// Применение фильтров
[dateFrom, dateTo, userFilter, pageFilter, groupBy].forEach(filter => {
filter.addEventListener('change', applyFilters);
});

userFilter.addEventListener('input', applyFilters);
pageFilter.addEventListener('input', applyFilters);

// Навигация по календарю
prevMonthBtn.addEventListener('click', function() {
currentCalendarDate.setMonth(currentCalendarDate.getMonth() - 1);
updateCalendar();
});

nextMonthBtn.addEventListener('click', function() {
currentCalendarDate.setMonth(currentCalendarDate.getMonth() + 1);
updateCalendar();
});

function applyFilters() {
filteredLogs = allLogs.filter(log => {
// Фильтр по дате
const logDate = new Date(log.date);
const fromDate = dateFrom.value ? new Date(dateFrom.value) : new Date(dateRange.min);
const toDate = dateTo.value ? new Date(dateTo.value) : new Date(dateRange.max);

const dateMatch = logDate >= fromDate && logDate <= toDate;

// Фильтр по пользователю
const userMatch = !userFilter.value ||
log.user.toLowerCase().includes(userFilter.value.toLowerCase());

// Фильтр по странице
const pageMatch = !pageFilter.value ||
log.page.toLowerCase().includes(pageFilter.value.toLowerCase());

return dateMatch && userMatch && pageMatch;
});

updateStatsTable();
updateCharts();
updatePopularPagesList();
}

function updateStatsTable() {
statsTableBody.innerHTML = '';

filteredLogs.forEach(log => {
const row = document.createElement('tr');

const dateCell = document.createElement('td');
dateCell.textContent = log.date;
row.appendChild(dateCell);

const timeCell = document.createElement('td');
timeCell.textContent = log.time;
row.appendChild(timeCell);

const userCell = document.createElement('td');
userCell.textContent = log.user;
row.appendChild(userCell);

const ipCell = document.createElement('td');
ipCell.textContent = log.ip;
row.appendChild(ipCell);

const methodCell = document.createElement('td');
methodCell.textContent = log.method;
row.appendChild(methodCell);

const pageCell = document.createElement('td');
pageCell.textContent = log.page;
row.appendChild(pageCell);

const codeCell = document.createElement('td');
codeCell.textContent = log.statusCode;
row.appendChild(codeCell);

statsTableBody.appendChild(row);
});
}

function updateCharts() {
// Обновление графика по датам с группировкой
const visitsByPeriod = {};
filteredLogs.forEach(log => {
let periodKey;
const date = new Date(log.date);

switch(groupBy.value) {
case 'month':
periodKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
break;
case 'year':
periodKey = `${date.getFullYear()}`;
break;
default: // day
periodKey = log.date;
}

visitsByPeriod[periodKey] = (visitsByPeriod[periodKey] || 0) + 1;
});

const periods = Object.keys(visitsByPeriod).sort();
const visits = periods.map(period => visitsByPeriod[period]);

visitsByDateChart.data.labels = periods;
visitsByDateChart.data.datasets[0].data = visits;
visitsByDateChart.update();

// Обновление графика популярных страниц
const pageCounts = {};
filteredLogs.forEach(log => {
pageCounts[log.page] = (pageCounts[log.page] || 0) + 1;
});

const sortedPages = Object.entries(pageCounts)
.sort((a, b) => b[1] - a[1])
.slice(0, 7);

popularPagesChart.data.labels = sortedPages.map(item => {
// Сокращаем длинные названия страниц для легенды
return item[0].length > 30 ? item[0].substring(0, 30) + '...' : item[0];
});
popularPagesChart.data.datasets[0].data = sortedPages.map(item => item[1]);
popularPagesChart.update();
}

function updatePopularPagesList() {
const pageCounts = {};
filteredLogs.forEach(log => {
pageCounts[log.page] = (pageCounts[log.page] || 0) + 1;
});

const sortedPages = Object.entries(pageCounts)
.sort((a, b) => b[1] - a[1])
.slice(0, 10);

popularPagesList.innerHTML = '';

sortedPages.forEach(([page, count]) => {
const pageItem = document.createElement('div');
pageItem.className = 'page-item';

const pageName = document.createElement('span');
pageName.textContent = page;

const pageCount = document.createElement('span');
pageCount.textContent = count;
pageCount.style.fontWeight = 'bold';
pageCount.style.color = '#3498db';

pageItem.appendChild(pageName);
pageItem.appendChild(pageCount);
popularPagesList.appendChild(pageItem);
});
}

function updateCalendar() {
// Обновляем заголовок календаря
const monthNames = ["Январь", "Февраль", "Март", "Апрель", "Май", "Июнь",
"Июль", "Август", "Сентябрь", "Октябрь", "Ноябрь", "Декабрь"];
calendarTitle.textContent = `${monthNames[currentCalendarDate.getMonth()]} ${currentCalendarDate.getFullYear()}`;

// Получаем уникальные даты с данными
const datesWithData = [...new Set(allLogs.map(log => log.date))].sort();

// Создаем объект для быстрой проверки наличия данных по дате
const dataDates = {};
datesWithData.forEach(date => {
dataDates[date] = true;
});

// Очищаем календарь
calendar.innerHTML = '';

// Добавляем заголовки дней недели
const daysOfWeek = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'];
daysOfWeek.forEach(day => {
const dayHeader = document.createElement('div');
dayHeader.className = 'calendar-day-header';
dayHeader.textContent = day;
calendar.appendChild(dayHeader);
});

// Определяем первый и последний день месяца для отображения
const firstDayOfMonth = new Date(currentCalendarDate.getFullYear(), currentCalendarDate.getMonth(), 1);
const lastDayOfMonth = new Date(currentCalendarDate.getFullYear(), currentCalendarDate.getMonth() + 1, 0);

// Определяем день недели первого дня месяца (0 - воскресенье, 1 - понедельник и т.д.)
let firstDayOfWeek = firstDayOfMonth.getDay();
// Преобразуем к формату Пн=0, Вт=1, ..., Вс=6
firstDayOfWeek = firstDayOfWeek === 0 ? 6 : firstDayOfWeek - 1;

// Добавляем пустые ячейки для дней предыдущего месяца
for (let i = 0; i < firstDayOfWeek; i++) {
const emptyDay = document.createElement('div');
emptyDay.className = 'calendar-day other-month';
calendar.appendChild(emptyDay);
}

// Заполняем календарь днями текущего месяца
for (let day = 1; day <= lastDayOfMonth.getDate(); day++) {
const dayElement = document.createElement('div');
dayElement.className = 'calendar-day';

const dateString = `${currentCalendarDate.getFullYear()}-${String(currentCalendarDate.getMonth() + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
dayElement.textContent = day;
dayElement.dataset.date = dateString;

if (dataDates[dateString]) {
dayElement.classList.add('has-data');
dayElement.title = `Есть данные за ${dateString}`;

// Добавляем обработчик клика
dayElement.addEventListener('click', function() {
// Снимаем выделение с предыдущей выбранной даты
if (selectedDate) {
const prevSelected = document.querySelector(`.calendar-day[data-date="${selectedDate}"]`);
if (prevSelected) {
prevSelected.classList.remove('selected');
}
}

// Выделяем новую дату
dayElement.classList.add('selected');
selectedDate = dateString;

// Устанавливаем фильтр на выбранную дату
dateFrom.value = dateString;
dateTo.value = dateString;
applyFilters();
});
}

calendar.appendChild(dayElement);
}
}

function getDateRange(logs) {
const dates = logs.map(log => new Date(log.date));
const minDate = new Date(Math.min(...dates));
const maxDate = new Date(Math.max(...dates));

return {
min: minDate.toISOString().split('T')[0],
max: maxDate.toISOString().split('T')[0]
};
}

function setDateFilters(dateRange) {
dateFrom.value = dateRange.min;
dateTo.value = dateRange.max;
}

function showError(message) {
errorDiv.textContent = message;
errorDiv.style.display = 'block';
}

// Загружаем кешированные данные при запуске
loadCachedData();
});
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Статистика посещений сайта</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}

body {
background-color: #f5f7fa;
color: #333;
line-height: 1.6;
padding: 20px;
}

.container {
max-width: 1400px;
margin: 0 auto;
background-color: white;
border-radius: 10px;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
padding: 25px;
}

header {
text-align: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 1px solid #eaeaea;
}

h1 {
color: #2c3e50;
margin-bottom: 10px;
}

.description {
color: #7f8c8d;
max-width: 800px;
margin: 0 auto;
}

.upload-section {
background-color: #f8f9fa;
padding: 20px;
border-radius: 8px;
margin-bottom: 30px;
border: 1px dashed #dee2e6;
}

.upload-label {
display: block;
margin-bottom: 15px;
font-weight: 600;
color: #495057;
}

.btn {
background-color: #3498db;
color: white;
border: none;
padding: 12px 25px;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
font-weight: 600;
transition: background-color 0.3s;
}

.btn:hover {
background-color: #2980b9;
}

.btn-secondary {
background-color: #6c757d;
}

.btn-secondary:hover {
background-color: #5a6268;
}

.file-input-wrapper {
position: relative;
display: inline-block;
margin-right: 10px;
}

.file-input {
position: absolute;
left: 0;
top: 0;
opacity: 0;
width: 100%;
height: 100%;
cursor: pointer;
}

.filters {
display: flex;
flex-wrap: wrap;
gap: 15px;
margin-bottom: 25px;
padding: 15px;
background-color: #f8f9fa;
border-radius: 8px;
}

.filter-group {
flex: 1;
min-width: 200px;
}

.filter-label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #495057;
}

.filter-input, .filter-select {
width: 100%;
padding: 10px;
border: 1px solid #ced4da;
border-radius: 4px;
}

.stats-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 25px;
margin-bottom: 30px;
}

.stats-card {
background-color: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}

.stats-card h3 {
margin-bottom: 15px;
color: #2c3e50;
border-bottom: 1px solid #eaeaea;
padding-bottom: 10px;
}

.chart-container {
height: 300px;
margin-top: 10px;
}

.popular-pages-list {
margin-top: 15px;
max-height: 250px;
overflow-y: auto;
}

.page-item {
display: flex;
justify-content: space-between;
padding: 8px 10px;
border-bottom: 1px solid #f0f0f0;
}

.page-item:hover {
background-color: #f8f9fa;
}

.table-container {
overflow-x: auto;
margin-top: 30px;
}

table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
}

th, td {
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid #eaeaea;
}

th {
background-color: #f8f9fa;
font-weight: 600;
color: #495057;
}

tr:hover {
background-color: #f8f9fa;
}

.loading {
text-align: center;
padding: 20px;
display: none;
}

.error {
color: #e74c3c;
padding: 10px;
background-color: #fadbd8;
border-radius: 4px;
margin-top: 10px;
display: none;
}

.calendar-container {
margin-top: 30px;
padding: 20px;
background-color: #f8f9fa;
border-radius: 8px;
}

.calendar-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}

.calendar-title {
font-weight: 600;
color: #495057;
font-size: 18px;
}

.calendar-nav {
display: flex;
gap: 10px;
}

.calendar-nav-btn {
background-color: #3498db;
color: white;
border: none;
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
font-weight: 600;
}

.calendar {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 5px;
}

.calendar-day {
padding: 8px;
text-align: center;
border-radius: 4px;
background-color: #e9ecef;
cursor: pointer;
transition: all 0.2s;
}

.calendar-day.has-data {
background-color: #3498db;
color: white;
font-weight: bold;
}

.calendar-day.has-data:hover {
background-color: #2980b9;
transform: scale(1.05);
}

.calendar-day.selected {
background-color: #e74c3c;
color: white;
}

.calendar-day.other-month {
background-color: #f8f9fa;
color: #adb5bd;
}

.calendar-day-header {
padding: 8px;
text-align: center;
font-weight: bold;
background-color: #dee2e6;
}

.period-selector {
display: flex;
gap: 10px;
margin-bottom: 15px;
}

.file-info {
margin-top: 10px;
padding: 10px;
background-color: #e9ecef;
border-radius: 4px;
font-size: 14px;
}

@media (max-width: 768px) {
.stats-container {
grid-template-columns: 1fr;
}

.filters {
flex-direction: column;
}

.period-selector {
flex-direction: column;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>Статистика посещений сайта</h1>
<p class="description">Загрузите логи IIS 10.0 для анализа посещений. Система автоматически отфильтрует запросы к JS, CSS и другим ресурсам, оставив только основные страницы.</p>
</header>

<section class="upload-section">
<label class="upload-label">Выберите файлы логов или папку:</label>
<div class="file-input-wrapper">
<button class="btn">Выбрать файлы</button>
<input type="file" id="logFiles" class="file-input" multiple accept=".log,.txt">
</div>
<div class="file-input-wrapper">
<button class="btn btn-secondary">Выбрать папку</button>
<input type="file" id="logFolder" class="file-input" webkitdirectory directory multiple>
</div>
<button id="loadLogs" class="btn">Загрузить логи</button>
<button id="clearCache" class="btn btn-secondary">Очистить кеш</button>
<div id="fileInfo" class="file-info"></div>
<div id="loading" class="loading">Загрузка и анализ логов...</div>
<div id="error" class="error"></div>
</section>

<section class="filters">
<div class="filter-group">
<label class="filter-label" for="dateFrom">Период с:</label>
<input type="date" id="dateFrom" class="filter-input">
</div>
<div class="filter-group">
<label class="filter-label" for="dateTo">Период по:</label>
<input type="date" id="dateTo" class="filter-input">
</div>
<div class="filter-group">
<label class="filter-label" for="userFilter">Фильтр по пользователю:</label>
<input type="text" id="userFilter" class="filter-input" placeholder="Введите имя пользователя">
</div>
<div class="filter-group">
<label class="filter-label" for="pageFilter">Фильтр по странице:</label>
<input type="text" id="pageFilter" class="filter-input" placeholder="Введите часть URL">
</div>
</section>

<div class="period-selector">
<div class="filter-group">
<label class="filter-label" for="groupBy">Группировать по:</label>
<select id="groupBy" class="filter-select">
<option value="day">Дням</option>
<option value="month">Месяцам</option>
<option value="year">Годам</option>
</select>
</div>
</div>

<div class="stats-container">
<div class="stats-card">
<h3>Посещения по датам</h3>
<div class="chart-container">
<canvas id="visitsByDateChart"></canvas>
</div>
</div>
<div class="stats-card">
<h3>Топ посещаемых страниц</h3>
<div class="chart-container">
<canvas id="popularPagesChart"></canvas>
</div>
<div class="popular-pages-list" id="popularPagesList">
<!-- Список популярных страниц будет здесь -->
</div>
</div>
</div>

<div class="calendar-container">
<div class="calendar-header">
<button id="prevMonth" class="calendar-nav-btn">←</button>
<h3 class="calendar-title" id="calendarTitle">Октябрь 2025</h3>
<button id="nextMonth" class="calendar-nav-btn">→</button>
</div>
<div class="calendar" id="calendar">
<!-- Календарь будет сгенерирован здесь -->
</div>
</div>

<div class="table-container">
<h3>Детальная статистика посещений</h3>
<table id="statsTable">
<thead>
<tr>
<th>Дата</th>
<th>Время</th>
<th>Пользователь</th>
<th>IP-адрес</th>
<th>Метод</th>
<th>Страница</th>
<th>Код ответа</th>
</tr>
</thead>
<tbody id="statsTableBody">
<!-- Данные будут загружены здесь -->
</tbody>
</table>
</div>
</div>

<script>
document.addEventListener('DOMContentLoaded', function() {
const loadLogsBtn = document.getElementById('loadLogs');
const logFilesInput = document.getElementById('logFiles');
const logFolderInput = document.getElementById('logFolder');
const clearCacheBtn = document.getElementById('clearCache');
const loadingDiv = document.getElementById('loading');
const errorDiv = document.getElementById('error');
const fileInfoDiv = document.getElementById('fileInfo');
const statsTableBody = document.getElementById('statsTableBody');
const dateFrom = document.getElementById('dateFrom');
const dateTo = document.getElementById('dateTo');
const userFilter = document.getElementById('userFilter');
const pageFilter = document.getElementById('pageFilter');
const groupBy = document.getElementById('groupBy');
const popularPagesList = document.getElementById('popularPagesList');
const calendar = document.getElementById('calendar');
const calendarTitle = document.getElementById('calendarTitle');
const prevMonthBtn = document.getElementById('prevMonth');
const nextMonthBtn = document.getElementById('nextMonth');

let allLogs = [];
let filteredLogs = [];
let dateRange = {};
let currentCalendarDate = new Date();
let selectedDate = null;
let selectedFiles = [];

// Инициализация графиков
const visitsByDateChart = new Chart(
document.getElementById('visitsByDateChart'),
{
type: 'bar',
data: {
labels: [],
datasets: [{
label: 'Количество посещений',
data: [],
backgroundColor: 'rgba(54, 162, 235, 0.5)',
borderColor: 'rgba(54, 162, 235, 1)',
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true
}
}
}
}
);

const popularPagesChart = new Chart(
document.getElementById('popularPagesChart'),
{
type: 'pie',
data: {
labels: [],
datasets: [{
data: [],
backgroundColor: [
'rgba(255, 99, 132, 0.5)',
'rgba(54, 162, 235, 0.5)',
'rgba(255, 206, 86, 0.5)',
'rgba(75, 192, 192, 0.5)',
'rgba(153, 102, 255, 0.5)',
'rgba(255, 159, 64, 0.5)',
'rgba(199, 199, 199, 0.5)'
],
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'right',
}
}
}
}
);

// Загрузка кешированных данных
function loadCachedData() {
const cachedLogs = localStorage.getItem('iisLogsData');
const cachedPath = localStorage.getItem('iisLogsPath');

if (cachedLogs && cachedPath) {
allLogs = JSON.parse(cachedLogs);
dateRange = getDateRange(allLogs);
setDateFilters(dateRange);
currentCalendarDate = new Date(dateRange.max);
updateCalendar();
applyFilters();

fileInfoDiv.textContent = `Загружены кешированные данные из: ${cachedPath}`;
}
}

// Сохранение данных в кеш
function saveToCache(data, path) {
localStorage.setItem('iisLogsData', JSON.stringify(data));
localStorage.setItem('iisLogsPath', path);
}

// Очистка кеша
clearCacheBtn.addEventListener('click', function() {
localStorage.removeItem('iisLogsData');
localStorage.removeItem('iisLogsPath');
allLogs = [];
filteredLogs = [];
statsTableBody.innerHTML = '';
fileInfoDiv.textContent = '';
visitsByDateChart.data.labels = [];
visitsByDateChart.data.datasets[0].data = [];
visitsByDateChart.update();
popularPagesChart.data.labels = [];
popularPagesChart.data.datasets[0].data = [];
popularPagesChart.update();
popularPagesList.innerHTML = '';
calendar.innerHTML = '';
alert('Кеш очищен');
});

// Обработка выбора файлов
logFilesInput.addEventListener('change', function(e) {
selectedFiles = Array.from(e.target.files);
updateFileInfo();
});

// Обработка выбора папки
logFolderInput.addEventListener('change', function(e) {
selectedFiles = Array.from(e.target.files);
updateFileInfo();
});

// Обновление информации о выбранных файлах
function updateFileInfo() {
if (selectedFiles.length > 0) {
fileInfoDiv.textContent = `Выбрано файлов: ${selectedFiles.length}`;
} else {
fileInfoDiv.textContent = '';
}
}

// Загрузка логов
loadLogsBtn.addEventListener('click', function() {
if (selectedFiles.length === 0) {
showError('Пожалуйста, выберите файлы логов или папку');
return;
}

loadingDiv.style.display = 'block';
errorDiv.style.display = 'none';

// Чтение файлов
const fileReaders = [];

selectedFiles.forEach(file => {
const reader = new FileReader();
fileReaders.push(new Promise((resolve) => {
reader.onload = function(e) {
resolve({
name: file.name,
content: e.target.result
});
};
reader.readAsText(file);
}));
});

Promise.all(fileReaders)
.then(fileContents => {
try {
// Парсинг логов из всех файлов
allLogs = [];

fileContents.forEach(file => {
const logs = parseLogFile(file.content, file.name);
allLogs = allLogs.concat(logs);
});

if (allLogs.length === 0) {
throw new Error('Не удалось распознать данные в логах. Проверьте формат файлов.');
}

// Сохраняем в кеш
const path = selectedFiles.length === 1 ?
selectedFiles[0].name :
`Папка с ${selectedFiles.length} файлами`;
saveToCache(allLogs, path);

dateRange = getDateRange(allLogs);
setDateFilters(dateRange);
currentCalendarDate = new Date(dateRange.max);
updateCalendar();
applyFilters();
loadingDiv.style.display = 'none';
} catch (error) {
loadingDiv.style.display = 'none';
showError('Ошибка при обработке логов: ' + error.message);
}
})
.catch(error => {
loadingDiv.style.display = 'none';
showError('Ошибка при чтении файлов: ' + error.message);
});
});

// Парсинг файла логов - УПРОЩЕННАЯ ВЕРСИЯ
function parseLogFile(content, filename) {
const lines = content.split('\n');
const logs = [];

for (const line of lines) {
if (!line.trim()) continue;

// Простой парсинг по пробелам - берем первые 10 полей
const parts = line.split(/\s+/);
if (parts.length < 10) continue;

try {
const date = parts[0];
const time = parts[1];
const ip = parts[2];
const method = parts[3];
const page = parts[4];
const query = parts[5] !== '-' ? parts[5] : '';
const port = parts[6];
const user = parts[7];
const clientIp = parts[8];
const userAgent = parts[9];
const statusCode = parts[10];

// Проверяем, что это валидная запись
if (date && time && ip && method && page && statusCode) {
// Фильтруем только успешные запросы и основные страницы
if (statusCode === '200' &&
!page.match(/\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$/i) &&
page !== '/favicon.ico') {

logs.push({
date: date,
time: time,
user: user !== '-' ? user : 'Неизвестно',
ip: clientIp,
method: method,
page: page + (query ? '?' + query : ''),
statusCode: parseInt(statusCode),
filename: filename
});
}
}
} catch (e) {
console.log('Ошибка парсинга строки:', line);
continue;
}
}

return logs;
}

// Применение фильтров
[dateFrom, dateTo, userFilter, pageFilter, groupBy].forEach(filter => {
filter.addEventListener('change', applyFilters);
});

userFilter.addEventListener('input', applyFilters);
pageFilter.addEventListener('input', applyFilters);

// Навигация по календарю
prevMonthBtn.addEventListener('click', function() {
currentCalendarDate.setMonth(currentCalendarDate.getMonth() - 1);
updateCalendar();
});

nextMonthBtn.addEventListener('click', function() {
currentCalendarDate.setMonth(currentCalendarDate.getMonth() + 1);
updateCalendar();
});

function applyFilters() {
filteredLogs = allLogs.filter(log => {
// Фильтр по дате
const logDate = new Date(log.date);
const fromDate = dateFrom.value ? new Date(dateFrom.value) : new Date(dateRange.min);
const toDate = dateTo.value ? new Date(dateTo.value) : new Date(dateRange.max);

const dateMatch = logDate >= fromDate && logDate <= toDate;

// Фильтр по пользователю
const userMatch = !userFilter.value ||
log.user.toLowerCase().includes(userFilter.value.toLowerCase());

// Фильтр по странице
const pageMatch = !pageFilter.value ||
log.page.toLowerCase().includes(pageFilter.value.toLowerCase());

return dateMatch && userMatch && pageMatch;
});

updateStatsTable();
updateCharts();
updatePopularPagesList();
}

function updateStatsTable() {
statsTableBody.innerHTML = '';

filteredLogs.forEach(log => {
const row = document.createElement('tr');

const dateCell = document.createElement('td');
dateCell.textContent = log.date;
row.appendChild(dateCell);

const timeCell = document.createElement('td');
timeCell.textContent = log.time;
row.appendChild(timeCell);

const userCell = document.createElement('td');
userCell.textContent = log.user;
row.appendChild(userCell);

const ipCell = document.createElement('td');
ipCell.textContent = log.ip;
row.appendChild(ipCell);

const methodCell = document.createElement('td');
methodCell.textContent = log.method;
row.appendChild(methodCell);

const pageCell = document.createElement('td');
pageCell.textContent = log.page;
row.appendChild(pageCell);

const codeCell = document.createElement('td');
codeCell.textContent = log.statusCode;
row.appendChild(codeCell);

statsTableBody.appendChild(row);
});
}

function updateCharts() {
// Обновление графика по датам с группировкой
const visitsByPeriod = {};
filteredLogs.forEach(log => {
let periodKey;
const date = new Date(log.date);

switch(groupBy.value) {
case 'month':
periodKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
break;
case 'year':
periodKey = `${date.getFullYear()}`;
break;
default: // day
periodKey = log.date;
}

visitsByPeriod[periodKey] = (visitsByPeriod[periodKey] || 0) + 1;
});

const periods = Object.keys(visitsByPeriod).sort();
const visits = periods.map(period => visitsByPeriod[period]);

visitsByDateChart.data.labels = periods;
visitsByDateChart.data.datasets[0].data = visits;
visitsByDateChart.update();

// Обновление графика популярных страниц
const pageCounts = {};
filteredLogs.forEach(log => {
pageCounts[log.page] = (pageCounts[log.page] || 0) + 1;
});

const sortedPages = Object.entries(pageCounts)
.sort((a, b) => b[1] - a[1])
.slice(0, 7);

popularPagesChart.data.labels = sortedPages.map(item => {
// Сокращаем длинные названия страниц для легенды
return item[0].length > 30 ? item[0].substring(0, 30) + '...' : item[0];
});
popularPagesChart.data.datasets[0].data = sortedPages.map(item => item[1]);
popularPagesChart.update();
}

function updatePopularPagesList() {
const pageCounts = {};
filteredLogs.forEach(log => {
pageCounts[log.page] = (pageCounts[log.page] || 0) + 1;
});

const sortedPages = Object.entries(pageCounts)
.sort((a, b) => b[1] - a[1])
.slice(0, 10);

popularPagesList.innerHTML = '';

sortedPages.forEach(([page, count]) => {
const pageItem = document.createElement('div');
pageItem.className = 'page-item';

const pageName = document.createElement('span');
pageName.textContent = page;

const pageCount = document.createElement('span');
pageCount.textContent = count;
pageCount.style.fontWeight = 'bold';
pageCount.style.color = '#3498db';

pageItem.appendChild(pageName);
pageItem.appendChild(pageCount);
popularPagesList.appendChild(pageItem);
});
}

function updateCalendar() {
// Обновляем заголовок календаря
const monthNames = ["Январь", "Февраль", "Март", "Апрель", "Май", "Июнь",
"Июль", "Август", "Сентябрь", "Октябрь", "Ноябрь", "Декабрь"];
calendarTitle.textContent = `${monthNames[currentCalendarDate.getMonth()]} ${currentCalendarDate.getFullYear()}`;

// Получаем уникальные даты с данными
const datesWithData = [...new Set(allLogs.map(log => log.date))].sort();

// Создаем объект для быстрой проверки наличия данных по дате
const dataDates = {};
datesWithData.forEach(date => {
dataDates[date] = true;
});

// Очищаем календарь
calendar.innerHTML = '';

// Добавляем заголовки дней недели
const daysOfWeek = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'];
daysOfWeek.forEach(day => {
const dayHeader = document.createElement('div');
dayHeader.className = 'calendar-day-header';
dayHeader.textContent = day;
calendar.appendChild(dayHeader);
});

// Определяем первый и последний день месяца для отображения
const firstDayOfMonth = new Date(currentCalendarDate.getFullYear(), currentCalendarDate.getMonth(), 1);
const lastDayOfMonth = new Date(currentCalendarDate.getFullYear(), currentCalendarDate.getMonth() + 1, 0);

// Определяем день недели первого дня месяца (0 - воскресенье, 1 - понедельник и т.д.)
let firstDayOfWeek = firstDayOfMonth.getDay();
// Преобразуем к формату Пн=0, Вт=1, ..., Вс=6
firstDayOfWeek = firstDayOfWeek === 0 ? 6 : firstDayOfWeek - 1;

// Добавляем пустые ячейки для дней предыдущего месяца
for (let i = 0; i < firstDayOfWeek; i++) {
const emptyDay = document.createElement('div');
emptyDay.className = 'calendar-day other-month';
calendar.appendChild(emptyDay);
}

// Заполняем календарь днями текущего месяца
for (let day = 1; day <= lastDayOfMonth.getDate(); day++) {
const dayElement = document.createElement('div');
dayElement.className = 'calendar-day';

const dateString = `${currentCalendarDate.getFullYear()}-${String(currentCalendarDate.getMonth() + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
dayElement.textContent = day;
dayElement.dataset.date = dateString;

if (dataDates[dateString]) {
dayElement.classList.add('has-data');
dayElement.title = `Есть данные за ${dateString}`;

// Добавляем обработчик клика
dayElement.addEventListener('click', function() {
// Снимаем выделение с предыдущей выбранной даты
if (selectedDate) {
const prevSelected = document.querySelector(`.calendar-day[data-date="${selectedDate}"]`);
if (prevSelected) {
prevSelected.classList.remove('selected');
}
}

// Выделяем новую дату
dayElement.classList.add('selected');
selectedDate = dateString;

// Устанавливаем фильтр на выбранную дату
dateFrom.value = dateString;
dateTo.value = dateString;
applyFilters();
});
}

calendar.appendChild(dayElement);
}
}

function getDateRange(logs) {
const dates = logs.map(log => new Date(log.date));
const minDate = new Date(Math.min(...dates));
const maxDate = new Date(Math.max(...dates));

return {
min: minDate.toISOString().split('T')[0],
max: maxDate.toISOString().split('T')[0]
};
}

function setDateFilters(dateRange) {
dateFrom.value = dateRange.min;
dateTo.value = dateRange.max;
}

function showError(message) {
errorDiv.textContent = message;
errorDiv.style.display = 'block';
}

// Загружаем кешированные данные при запуске
loadCachedData();
});
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Статистика посещений сайта</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}

body {
background-color: #f5f7fa;
color: #333;
line-height: 1.6;
padding: 20px;
}

.container {
max-width: 1400px;
margin: 0 auto;
background-color: white;
border-radius: 10px;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
padding: 25px;
}

header {
text-align: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 1px solid #eaeaea;
}

h1 {
color: #2c3e50;
margin-bottom: 10px;
}

.description {
color: #7f8c8d;
max-width: 800px;
margin: 0 auto;
}

.upload-section {
background-color: #f8f9fa;
padding: 20px;
border-radius: 8px;
margin-bottom: 30px;
border: 1px dashed #dee2e6;
}

.upload-label {
display: block;
margin-bottom: 15px;
font-weight: 600;
color: #495057;
}

.btn {
background-color: #3498db;
color: white;
border: none;
padding: 12px 25px;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
font-weight: 600;
transition: background-color 0.3s;
}

.btn:hover {
background-color: #2980b9;
}

.btn-secondary {
background-color: #6c757d;
}

.btn-secondary:hover {
background-color: #5a6268;
}

.file-input-wrapper {
position: relative;
display: inline-block;
margin-right: 10px;
}

.file-input {
position: absolute;
left: 0;
top: 0;
opacity: 0;
width: 100%;
height: 100%;
cursor: pointer;
}

.filters {
display: flex;
flex-wrap: wrap;
gap: 15px;
margin-bottom: 25px;
padding: 15px;
background-color: #f8f9fa;
border-radius: 8px;
}

.filter-group {
flex: 1;
min-width: 200px;
}

.filter-label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #495057;
}

.filter-input, .filter-select {
width: 100%;
padding: 10px;
border: 1px solid #ced4da;
border-radius: 4px;
}

.stats-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 25px;
margin-bottom: 30px;
}

.stats-card {
background-color: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}

.stats-card h3 {
margin-bottom: 15px;
color: #2c3e50;
border-bottom: 1px solid #eaeaea;
padding-bottom: 10px;
}

.chart-container {
height: 300px;
margin-top: 10px;
}

.popular-pages-list {
margin-top: 15px;
max-height: 250px;
overflow-y: auto;
}

.page-item {
display: flex;
justify-content: space-between;
padding: 8px 10px;
border-bottom: 1px solid #f0f0f0;
}

.page-item:hover {
background-color: #f8f9fa;
}

.table-container {
overflow-x: auto;
margin-top: 30px;
}

table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
}

th, td {
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid #eaeaea;
}

th {
background-color: #f8f9fa;
font-weight: 600;
color: #495057;
}

tr:hover {
background-color: #f8f9fa;
}

.loading {
text-align: center;
padding: 20px;
display: none;
}

.error {
color: #e74c3c;
padding: 10px;
background-color: #fadbd8;
border-radius: 4px;
margin-top: 10px;
display: none;
}

.debug {
color: #666;
padding: 10px;
background-color: #f8f9fa;
border-radius: 4px;
margin-top: 10px;
font-family: monospace;
font-size: 12px;
max-height: 200px;
overflow-y: auto;
display: none;
}

.calendar-container {
margin-top: 30px;
padding: 20px;
background-color: #f8f9fa;
border-radius: 8px;
}

.calendar-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}

.calendar-title {
font-weight: 600;
color: #495057;
font-size: 18px;
}

.calendar-nav {
display: flex;
gap: 10px;
}

.calendar-nav-btn {
background-color: #3498db;
color: white;
border: none;
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
font-weight: 600;
}

.calendar {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 5px;
}

.calendar-day {
padding: 8px;
text-align: center;
border-radius: 4px;
background-color: #e9ecef;
cursor: pointer;
transition: all 0.2s;
}

.calendar-day.has-data {
background-color: #3498db;
color: white;
font-weight: bold;
}

.calendar-day.has-data:hover {
background-color: #2980b9;
transform: scale(1.05);
}

.calendar-day.selected {
background-color: #e74c3c;
color: white;
}

.calendar-day.other-month {
background-color: #f8f9fa;
color: #adb5bd;
}

.calendar-day-header {
padding: 8px;
text-align: center;
font-weight: bold;
background-color: #dee2e6;
}

.period-selector {
display: flex;
gap: 10px;
margin-bottom: 15px;
}

.file-info {
margin-top: 10px;
padding: 10px;
background-color: #e9ecef;
border-radius: 4px;
font-size: 14px;
}

@media (max-width: 768px) {
.stats-container {
grid-template-columns: 1fr;
}

.filters {
flex-direction: column;
}

.period-selector {
flex-direction: column;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>Статистика посещений сайта</h1>
<p class="description">Загрузите логи IIS 10.0 для анализа посещений. Система автоматически отфильтрует запросы к JS, CSS и другим ресурсам, оставив только основные страницы.</p>
</header>

<section class="upload-section">
<label class="upload-label">Выберите файлы логов или папку:</label>
<div class="file-input-wrapper">
<button class="btn">Выбрать файлы</button>
<input type="file" id="logFiles" class="file-input" multiple accept=".log,.txt">
</div>
<div class="file-input-wrapper">
<button class="btn btn-secondary">Выбрать папку</button>
<input type="file" id="logFolder" class="file-input" webkitdirectory directory multiple>
</div>
<button id="loadLogs" class="btn">Загрузить логи</button>
<button id="clearCache" class="btn btn-secondary">Очистить кеш</button>
<button id="debugBtn" class="btn btn-secondary">Отладка</button>
<div id="fileInfo" class="file-info"></div>
<div id="loading" class="loading">Загрузка и анализ логов...</div>
<div id="error" class="error"></div>
<div id="debug" class="debug"></div>
</section>

<section class="filters">
<div class="filter-group">
<label class="filter-label" for="dateFrom">Период с:</label>
<input type="date" id="dateFrom" class="filter-input">
</div>
<div class="filter-group">
<label class="filter-label" for="dateTo">Период по:</label>
<input type="date" id="dateTo" class="filter-input">
</div>
<div class="filter-group">
<label class="filter-label" for="userFilter">Фильтр по пользователю:</label>
<input type="text" id="userFilter" class="filter-input" placeholder="Введите имя пользователя">
</div>
<div class="filter-group">
<label class="filter-label" for="pageFilter">Фильтр по странице:</label>
<input type="text" id="pageFilter" class="filter-input" placeholder="Введите часть URL">
</div>
</section>

<div class="period-selector">
<div class="filter-group">
<label class="filter-label" for="groupBy">Группировать по:</label>
<select id="groupBy" class="filter-select">
<option value="day">Дням</option>
<option value="month">Месяцам</option>
<option value="year">Годам</option>
</select>
</div>
</div>

<div class="stats-container">
<div class="stats-card">
<h3>Посещения по датам</h3>
<div class="chart-container">
<canvas id="visitsByDateChart"></canvas>
</div>
</div>
<div class="stats-card">
<h3>Топ посещаемых страниц</h3>
<div class="chart-container">
<canvas id="popularPagesChart"></canvas>
</div>
<div class="popular-pages-list" id="popularPagesList">
<!-- Список популярных страниц будет здесь -->
</div>
</div>
</div>

<div class="calendar-container">
<div class="calendar-header">
<button id="prevMonth" class="calendar-nav-btn">←</button>
<h3 class="calendar-title" id="calendarTitle">Октябрь 2025</h3>
<button id="nextMonth" class="calendar-nav-btn">→</button>
</div>
<div class="calendar" id="calendar">
<!-- Календарь будет сгенерирован здесь -->
</div>
</div>

<div class="table-container">
<h3>Детальная статистика посещений</h3>
<table id="statsTable">
<thead>
<tr>
<th>Дата</th>
<th>Время</th>
<th>Пользователь</th>
<th>IP-адрес</th>
<th>Метод</th>
<th>Страница</th>
<th>Код ответа</th>
</tr>
</thead>
<tbody id="statsTableBody">
<!-- Данные будут загружены здесь -->
</tbody>
</table>
</div>
</div>

<script>
document.addEventListener('DOMContentLoaded', function() {
const loadLogsBtn = document.getElementById('loadLogs');
const logFilesInput = document.getElementById('logFiles');
const logFolderInput = document.getElementById('logFolder');
const clearCacheBtn = document.getElementById('clearCache');
const debugBtn = document.getElementById('debugBtn');
const loadingDiv = document.getElementById('loading');
const errorDiv = document.getElementById('error');
const debugDiv = document.getElementById('debug');
const fileInfoDiv = document.getElementById('fileInfo');
const statsTableBody = document.getElementById('statsTableBody');
const dateFrom = document.getElementById('dateFrom');
const dateTo = document.getElementById('dateTo');
const userFilter = document.getElementById('userFilter');
const pageFilter = document.getElementById('pageFilter');
const groupBy = document.getElementById('groupBy');
const popularPagesList = document.getElementById('popularPagesList');
const calendar = document.getElementById('calendar');
const calendarTitle = document.getElementById('calendarTitle');
const prevMonthBtn = document.getElementById('prevMonth');
const nextMonthBtn = document.getElementById('nextMonth');

let allLogs = [];
let filteredLogs = [];
let dateRange = {};
let currentCalendarDate = new Date();
let selectedDate = null;
let selectedFiles = [];
let debugMode = false;

// Инициализация графиков
const visitsByDateChart = new Chart(
document.getElementById('visitsByDateChart'),
{
type: 'bar',
data: {
labels: [],
datasets: [{
label: 'Количество посещений',
data: [],
backgroundColor: 'rgba(54, 162, 235, 0.5)',
borderColor: 'rgba(54, 162, 235, 1)',
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true
}
}
}
}
);

const popularPagesChart = new Chart(
document.getElementById('popularPagesChart'),
{
type: 'pie',
data: {
labels: [],
datasets: [{
data: [],
backgroundColor: [
'rgba(255, 99, 132, 0.5)',
'rgba(54, 162, 235, 0.5)',
'rgba(255, 206, 86, 0.5)',
'rgba(75, 192, 192, 0.5)',
'rgba(153, 102, 255, 0.5)',
'rgba(255, 159, 64, 0.5)',
'rgba(199, 199, 199, 0.5)'
],
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'right',
}
}
}
}
);

// Переключение режима отладки
debugBtn.addEventListener('click', function() {
debugMode = !debugMode;
debugDiv.style.display = debugMode ? 'block' : 'none';
debugBtn.textContent = debugMode ? 'Скрыть отладку' : 'Отладка';
if (debugMode && selectedFiles.length > 0) {
showDebugInfo();
}
});

// Загрузка кешированных данных
function loadCachedData() {
const cachedLogs = localStorage.getItem('iisLogsData');
const cachedPath = localStorage.getItem('iisLogsPath');

if (cachedLogs && cachedPath) {
allLogs = JSON.parse(cachedLogs);
dateRange = getDateRange(allLogs);
setDateFilters(dateRange);
currentCalendarDate = new Date(dateRange.max);
updateCalendar();
applyFilters();

fileInfoDiv.textContent = `Загружены кешированные данные из: ${cachedPath}`;
}
}

// Сохранение данных в кеш
function saveToCache(data, path) {
localStorage.setItem('iisLogsData', JSON.stringify(data));
localStorage.setItem('iisLogsPath', path);
}

// Очистка кеша
clearCacheBtn.addEventListener('click', function() {
localStorage.removeItem('iisLogsData');
localStorage.removeItem('iisLogsPath');
allLogs = [];
filteredLogs = [];
statsTableBody.innerHTML = '';
fileInfoDiv.textContent = '';
visitsByDateChart.data.labels = [];
visitsByDateChart.data.datasets[0].data = [];
visitsByDateChart.update();
popularPagesChart.data.labels = [];
popularPagesChart.data.datasets[0].data = [];
popularPagesChart.update();
popularPagesList.innerHTML = '';
calendar.innerHTML = '';
alert('Кеш очищен');
});

// Обработка выбора файлов
logFilesInput.addEventListener('change', function(e) {
selectedFiles = Array.from(e.target.files);
updateFileInfo();
});

// Обработка выбора папки
logFolderInput.addEventListener('change', function(e) {
selectedFiles = Array.from(e.target.files);
updateFileInfo();
});

// Обновление информации о выбранных файлах
function updateFileInfo() {
if (selectedFiles.length > 0) {
fileInfoDiv.textContent = `Выбрано файлов: ${selectedFiles.length}`;
} else {
fileInfoDiv.textContent = '';
}
}

// Загрузка логов
loadLogsBtn.addEventListener('click', function() {
if (selectedFiles.length === 0) {
showError('Пожалуйста, выберите файлы логов или папку');
return;
}

loadingDiv.style.display = 'block';
errorDiv.style.display = 'none';
debugDiv.innerHTML = '';

// Чтение файлов
const fileReaders = [];

selectedFiles.forEach(file => {
const reader = new FileReader();
fileReaders.push(new Promise((resolve) => {
reader.onload = function(e) {
resolve({
name: file.name,
content: e.target.result
});
};
reader.readAsText(file);
}));
});

Promise.all(fileReaders)
.then(fileContents => {
try {
// Парсинг логов из всех файлов
allLogs = [];
let debugInfo = [];

fileContents.forEach(file => {
const result = parseLogFile(file.content, file.name);
allLogs = allLogs.concat(result.logs);
debugInfo = debugInfo.concat(result.debug);
});

if (allLogs.length === 0) {
throw new Error('Не удалось распознать данные в логах. Проверьте формат файлов.');
}

// Сохраняем в кеш
const path = selectedFiles.length === 1 ?
selectedFiles[0].name :
`Папка с ${selectedFiles.length} файлами`;
saveToCache(allLogs, path);

dateRange = getDateRange(allLogs);
setDateFilters(dateRange);
currentCalendarDate = new Date(dateRange.max);
updateCalendar();
applyFilters();
loadingDiv.style.display = 'none';

// Показываем отладочную информацию
if (debugMode) {
debugDiv.innerHTML = debugInfo.join('<br>');
}

} catch (error) {
loadingDiv.style.display = 'none';
showError('Ошибка при обработке логов: ' + error.message);
}
})
.catch(error => {
loadingDiv.style.display = 'none';
showError('Ошибка при чтении файлов: ' + error.message);
});
});

// Парсинг файла логов - УПРОЩЕННАЯ ВЕРСИЯ
function parseLogFile(content, filename) {
const lines = content.split('\n');
const logs = [];
const debug = [`Файл: ${filename}`, `Всего строк: ${lines.length}`];

let parsedCount = 0;
let sampleLines = [];

for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (!line) continue;

// Сохраняем несколько первых строк для отладки
if (i < 5) {
sampleLines.push(`Строка ${i+1}: ${line.substring(0, 100)}...`);
}

try {
// Простой парсинг по пробелам
const parts = line.split(/\s+/);

// Ищем дату в формате ГГГГ-ММ-ДД
let dateIndex = -1;
for (let j = 0; j < parts.length; j++) {
if (parts[j].match(/^\d{4}-\d{2}-\d{2}$/)) {
dateIndex = j;
break;
}
}

if (dateIndex === -1) continue;

const date = parts[dateIndex];
const time = parts[dateIndex + 1] || '';
const ip = parts[dateIndex + 2] || '';
const method = parts[dateIndex + 3] || '';
const page = parts[dateIndex + 4] || '';

// Ищем статус код (3 цифры)
let statusCode = '';
for (let j = dateIndex + 5; j < parts.length; j++) {
if (parts[j].match(/^\d{3}$/)) {
statusCode = parts[j];
break;
}
}

// Ищем пользователя (содержит обратный слеш)
let user = 'Неизвестно';
for (let j = dateIndex + 5; j < parts.length; j++) {
if (parts[j].includes('\\')) {
user = parts[j];
break;
}
}

if (date && time && statusCode === '200' &&
!page.match(/\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$/i)) {

logs.push({
date: date,
time: time,
user: user,
ip: ip,
method: method,
page: page,
statusCode: parseInt(statusCode),
filename: filename
});
parsedCount++;
}
} catch (e) {
// Пропускаем строки с ошибками
continue;
}
}

debug.push(...sampleLines);
debug.push(`Успешно распарсено: ${parsedCount} записей`);

return { logs, debug };
}

function showDebugInfo() {
if (selectedFiles.length > 0) {
const file = selectedFiles[0];
const reader = new FileReader();
reader.onload = function(e) {
const content = e.target.result;
const lines = content.split('\n');
let debugInfo = [
`Файл: ${file.name}`,
`Размер: ${file.size} байт`,
`Строк: ${lines.length}`,
'Первые 5 строк:'
];

for (let i = 0; i < Math.min(5, lines.length); i++) {
debugInfo.push(`[${i+1}] ${lines[i].substring(0, 200)}`);
}

debugDiv.innerHTML = debugInfo.join('<br>');
};
reader.readAsText(file);
}
}

// Применение фильтров
[dateFrom, dateTo, userFilter, pageFilter, groupBy].forEach(filter => {
filter.addEventListener('change', applyFilters);
});

userFilter.addEventListener('input', applyFilters);
pageFilter.addEventListener('input', applyFilters);

// Навигация по календарю
prevMonthBtn.addEventListener('click', function() {
currentCalendarDate.setMonth(currentCalendarDate.getMonth() - 1);
updateCalendar();
});

nextMonthBtn.addEventListener('click', function() {
currentCalendarDate.setMonth(currentCalendarDate.getMonth() + 1);
updateCalendar();
});

function applyFilters() {
filteredLogs = allLogs.filter(log => {
// Фильтр по дате
const logDate = new Date(log.date);
const fromDate = dateFrom.value ? new Date(dateFrom.value) : new Date(dateRange.min);
const toDate = dateTo.value ? new Date(dateTo.value) : new Date(dateRange.max);

const dateMatch = logDate >= fromDate && logDate <= toDate;

// Фильтр по пользователю
const userMatch = !userFilter.value ||
log.user.toLowerCase().includes(userFilter.value.toLowerCase());

// Фильтр по странице
const pageMatch = !pageFilter.value ||
log.page.toLowerCase().includes(pageFilter.value.toLowerCase());

return dateMatch && userMatch && pageMatch;
});

updateStatsTable();
updateCharts();
updatePopularPagesList();
}

function updateStatsTable() {
statsTableBody.innerHTML = '';

filteredLogs.forEach(log => {
const row = document.createElement('tr');

const dateCell = document.createElement('td');
dateCell.textContent = log.date;
row.appendChild(dateCell);

const timeCell = document.createElement('td');
timeCell.textContent = log.time;
row.appendChild(timeCell);

const userCell = document.createElement('td');
userCell.textContent = log.user;
row.appendChild(userCell);

const ipCell = document.createElement('td');
ipCell.textContent = log.ip;
row.appendChild(ipCell);

const methodCell = document.createElement('td');
methodCell.textContent = log.method;
row.appendChild(methodCell);

const pageCell = document.createElement('td');
pageCell.textContent = log.page;
row.appendChild(pageCell);

const codeCell = document.createElement('td');
codeCell.textContent = log.statusCode;
row.appendChild(codeCell);

statsTableBody.appendChild(row);
});
}

function updateCharts() {
// Обновление графика по датам с группировкой
const visitsByPeriod = {};
filteredLogs.forEach(log => {
let periodKey;
const date = new Date(log.date);

switch(groupBy.value) {
case 'month':
periodKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
break;
case 'year':
periodKey = `${date.getFullYear()}`;
break;
default: // day
periodKey = log.date;
}

visitsByPeriod[periodKey] = (visitsByPeriod[periodKey] || 0) + 1;
});

const periods = Object.keys(visitsByPeriod).sort();
const visits = periods.map(period => visitsByPeriod[period]);

visitsByDateChart.data.labels = periods;
visitsByDateChart.data.datasets[0].data = visits;
visitsByDateChart.update();

// Обновление графика популярных страниц
const pageCounts = {};
filteredLogs.forEach(log => {
pageCounts[log.page] = (pageCounts[log.page] || 0) + 1;
});

const sortedPages = Object.entries(pageCounts)
.sort((a, b) => b[1] - a[1])
.slice(0, 7);

popularPagesChart.data.labels = sortedPages.map(item => {
// Сокращаем длинные названия страниц для легенды
return item[0].length > 30 ? item[0].substring(0, 30) + '...' : item[0];
});
popularPagesChart.data.datasets[0].data = sortedPages.map(item => item[1]);
popularPagesChart.update();
}

function updatePopularPagesList() {
const pageCounts = {};
filteredLogs.forEach(log => {
pageCounts[log.page] = (pageCounts[log.page] || 0) + 1;
});

const sortedPages = Object.entries(pageCounts)
.sort((a, b) => b[1] - a[1])
.slice(0, 10);

popularPagesList.innerHTML = '';

sortedPages.forEach(([page, count]) => {
const pageItem = document.createElement('div');
pageItem.className = 'page-item';

const pageName = document.createElement('span');
pageName.textContent = page;

const pageCount = document.createElement('span');
pageCount.textContent = count;
pageCount.style.fontWeight = 'bold';
pageCount.style.color = '#3498db';

pageItem.appendChild(pageName);
pageItem.appendChild(pageCount);
popularPagesList.appendChild(pageItem);
});
}

function updateCalendar() {
// Обновляем заголовок календаря
const monthNames = ["Январь", "Февраль", "Март", "Апрель", "Май", "Июнь",
"Июль", "Август", "Сентябрь", "Октябрь", "Ноябрь", "Декабрь"];
calendarTitle.textContent = `${monthNames[currentCalendarDate.getMonth()]} ${currentCalendarDate.getFullYear()}`;

// Получаем уникальные даты с данными
const datesWithData = [...new Set(allLogs.map(log => log.date))].sort();

// Создаем объект для быстрой проверки наличия данных по дате
const dataDates = {};
datesWithData.forEach(date => {
dataDates[date] = true;
});

// Очищаем календарь
calendar.innerHTML = '';

// Добавляем заголовки дней недели
const daysOfWeek = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'];
daysOfWeek.forEach(day => {
const dayHeader = document.createElement('div');
dayHeader.className = 'calendar-day-header';
dayHeader.textContent = day;
calendar.appendChild(dayHeader);
});

// Определяем первый и последний день месяца для отображения
const firstDayOfMonth = new Date(currentCalendarDate.getFullYear(), currentCalendarDate.getMonth(), 1);
const lastDayOfMonth = new Date(currentCalendarDate.getFullYear(), currentCalendarDate.getMonth() + 1, 0);

// Определяем день недели первого дня месяца (0 - воскресенье, 1 - понедельник и т.д.)
let firstDayOfWeek = firstDayOfMonth.getDay();
// Преобразуем к формату Пн=0, Вт=1, ..., Вс=6
firstDayOfWeek = firstDayOfWeek === 0 ? 6 : firstDayOfWeek - 1;

// Добавляем пустые ячейки для дней предыдущего месяца
for (let i = 0; i < firstDayOfWeek; i++) {
const emptyDay = document.createElement('div');
emptyDay.className = 'calendar-day other-month';
calendar.appendChild(emptyDay);
}

// Заполняем календарь днями текущего месяца
for (let day = 1; day <= lastDayOfMonth.getDate(); day++) {
const dayElement = document.createElement('div');
dayElement.className = 'calendar-day';

const dateString = `${currentCalendarDate.getFullYear()}-${String(currentCalendarDate.getMonth() + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
dayElement.textContent = day;
dayElement.dataset.date = dateString;

if (dataDates[dateString]) {
dayElement.classList.add('has-data');
dayElement.title = `Есть данные за ${dateString}`;

// Добавляем обработчик клика
dayElement.addEventListener('click', function() {
// Снимаем выделение с предыдущей выбранной даты
if (selectedDate) {
const prevSelected = document.querySelector(`.calendar-day[data-date="${selectedDate}"]`);
if (prevSelected) {
prevSelected.classList.remove('selected');
}
}

// Выделяем новую дату
dayElement.classList.add('selected');
selectedDate = dateString;

// Устанавливаем фильтр на выбранную дату
dateFrom.value = dateString;
dateTo.value = dateString;
applyFilters();
});
}

calendar.appendChild(dayElement);
}
}

function getDateRange(logs) {
const dates = logs.map(log => new Date(log.date));
const minDate = new Date(Math.min(...dates));
const maxDate = new Date(Math.max(...dates));

return {
min: minDate.toISOString().split('T')[0],
max: maxDate.toISOString().split('T')[0]
};
}

function setDateFilters(dateRange) {
dateFrom.value = dateRange.min;
dateTo.value = dateRange.max;
}

function showError(message) {
errorDiv.textContent = message;
errorDiv.style.display = 'block';
}

// Загружаем кешированные данные при запуске
loadCachedData();
});
</script>
</body>
</html>
Поиск:
Новый ответ
Имя:
Текст сообщения: