Отзывы и предложения к софту от AleXStam
Поговорим о...
<!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;
}

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

.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;
}

.stats-toggle {
margin-bottom: 15px;
display: flex;
gap: 10px;
}

.toggle-btn {
background-color: #e9ecef;
border: 1px solid #ced4da;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
}

.toggle-btn.active {
background-color: #3498db;
color: white;
border-color: #3498db;
}

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

.filters {
flex-direction: column;
}

.period-selector {
flex-direction: column;
}

.stats-toggle {
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>

<div class="stats-toggle">
<button id="showAllVisits" class="toggle-btn active">Все посещения</button>
<button id="showUniqueVisits" class="toggle-btn">Уникальные посещения</button>
</div>

<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 showAllVisitsBtn = document.getElementById('showAllVisits');
const showUniqueVisitsBtn = document.getElementById('showUniqueVisits');
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 = [];
let showUnique = 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',
}
}
}
}
);

// Переключение между всеми и уникальными посещениями
showAllVisitsBtn.addEventListener('click', function() {
showUnique = false;
showAllVisitsBtn.classList.add('active');
showUniqueVisitsBtn.classList.remove('active');
applyFilters();
});

showUniqueVisitsBtn.addEventListener('click', function() {
showUnique = true;
showAllVisitsBtn.classList.remove('active');
showUniqueVisitsBtn.classList.add('active');
applyFilters();
});

// Загрузка кешированных данных
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, reject) => {
reader.onload = function(e) {
resolve({
name: file.name,
content: e.target.result
});
};
reader.onerror = function() {
reject(new Error(`Ошибка чтения файла: ${file.name}`));
};
reader.readAsText(file);
}));
});

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

fileContents.forEach(file => {
totalFiles++;
try {
const logs = parseLogFile(file.content, file.name);
if (logs.length > 0) {
allLogs = allLogs.concat(logs);
successfulFiles++;
}
} catch (e) {
console.warn(`Ошибка парсинга файла ${file.name}:`, e.message);
}
});

if (allLogs.length === 0) {
throw new Error(`Не удалось распознать данные ни в одном файле. Обработано ${successfulFiles}/${totalFiles} файлов`);
}

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

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

if (successfulFiles < totalFiles) {
showError(`Успешно обработано ${successfulFiles} из ${totalFiles} файлов. Некоторые файлы содержат ошибки.`);
}

} 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) {
const trimmedLine = line.trim();
if (!trimmedLine) continue;

// Пропускаем служебные строки (начинающиеся с #)
if (trimmedLine.startsWith('#')) continue;

try {
// Простой парсинг по пробелам
const parts = trimmedLine.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) &&
page !== '/favicon.ico' && page !== '/') {

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

return logs;
}

// Получение уникальных посещений (пользователь + страница + дата)
function getUniqueVisits(logs) {
const uniqueMap = new Map();
const uniqueLogs = [];

logs.forEach(log => {
const key = `${log.date}_${log.user}_${log.page}`;
if (!uniqueMap.has(key)) {
uniqueMap.set(key, true);
uniqueLogs.push(log);
}
});

return uniqueLogs;
}

// Применение фильтров
[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() {
let logsToFilter = showUnique ? getUniqueVisits(allLogs) : allLogs;

filteredLogs = logsToFilter.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.data.datasets[0].label = showUnique ? 'Уникальные посещения' : 'Все посещения';
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);

let firstDayOfWeek = firstDayOfMonth.getDay();
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;
}

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

.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;
position: relative;
}

.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.in-range {
background-color: #ffa07a;
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;
}

.stats-toggle {
margin-bottom: 15px;
display: flex;
gap: 10px;
}

.toggle-btn {
background-color: #e9ecef;
border: 1px solid #ced4da;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
}

.toggle-btn.active {
background-color: #3498db;
color: white;
border-color: #3498db;
}

.calendar-hint {
margin-top: 10px;
font-size: 14px;
color: #6c757d;
text-align: center;
}

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

.filters {
flex-direction: column;
}

.period-selector {
flex-direction: column;
}

.stats-toggle {
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>

<div class="stats-toggle">
<button id="showAllVisits" class="toggle-btn active">Все посещения</button>
<button id="showUniqueVisits" class="toggle-btn">Уникальные посещения</button>
</div>

<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 class="calendar-hint" id="calendarHint">
Кликните на дату для выбора одного дня. Зажмите Shift и кликните для выбора периода.
</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 showAllVisitsBtn = document.getElementById('showAllVisits');
const showUniqueVisitsBtn = document.getElementById('showUniqueVisits');
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 calendarHint = document.getElementById('calendarHint');
const prevMonthBtn = document.getElementById('prevMonth');
const nextMonthBtn = document.getElementById('nextMonth');

let allLogs = [];
let filteredLogs = [];
let dateRange = {};
let currentCalendarDate = new Date();
let selectedDateStart = null;
let selectedDateEnd = null;
let selectedFiles = [];
let showUnique = false;
let isShiftPressed = false;

// Отслеживаем нажатие Shift
document.addEventListener('keydown', function(e) {
if (e.key === 'Shift') {
isShiftPressed = true;
calendarHint.textContent = 'Shift нажат. Кликните на вторую дату для выбора периода.';
}
});

document.addEventListener('keyup', function(e) {
if (e.key === 'Shift') {
isShiftPressed = false;
calendarHint.textContent = 'Кликните на дату для выбора одного дня. Зажмите Shift и кликните для выбора периода.';
}
});

// Инициализация графиков
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',
}
}
}
}
);

// Переключение между всеми и уникальными посещениями
showAllVisitsBtn.addEventListener('click', function() {
showUnique = false;
showAllVisitsBtn.classList.add('active');
showUniqueVisitsBtn.classList.remove('active');
applyFilters();
});

showUniqueVisitsBtn.addEventListener('click', function() {
showUnique = true;
showAllVisitsBtn.classList.remove('active');
showUniqueVisitsBtn.classList.add('active');
applyFilters();
});

// Загрузка кешированных данных
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, reject) => {
reader.onload = function(e) {
resolve({
name: file.name,
content: e.target.result
});
};
reader.onerror = function() {
reject(new Error(`Ошибка чтения файла: ${file.name}`));
};
reader.readAsText(file);
}));
});

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

fileContents.forEach(file => {
totalFiles++;
try {
const logs = parseLogFile(file.content, file.name);
if (logs.length > 0) {
allLogs = allLogs.concat(logs);
successfulFiles++;
}
} catch (e) {
console.warn(`Ошибка парсинга файла ${file.name}:`, e.message);
}
});

if (allLogs.length === 0) {
throw new Error(`Не удалось распознать данные ни в одном файле. Обработано ${successfulFiles}/${totalFiles} файлов`);
}

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

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

if (successfulFiles < totalFiles) {
showError(`Успешно обработано ${successfulFiles} из ${totalFiles} файлов. Некоторые файлы содержат ошибки.`);
}

} 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 = [];
let currentFields = null;

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

// Обрабатываем заголовок Fields
if (trimmedLine.startsWith('#Fields:')) {
currentFields = trimmedLine.substring(8).trim().split(/\s+/);
continue;
}

// Пропускаем другие служебные строки
if (trimmedLine.startsWith('#')) continue;

try {
const parts = trimmedLine.split(/\s+/);

// Если у нас есть информация о полях, используем ее
if (currentFields) {
const logEntry = {};
let fieldIndex = 0;

for (let i = 0; i < parts.length && fieldIndex < currentFields.length; i++) {
const field = currentFields[fieldIndex];
logEntry[field] = parts[i];
fieldIndex++;
}

// Извлекаем данные из структурированной записи
const date = logEntry.date;
const time = logEntry.time;
const method = logEntry['cs-method'];
const page = logEntry['cs-uri-stem'];
const user = logEntry['cs-username'];
const ip = logEntry['c-ip'];
const statusCode = logEntry['sc-status'];

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

logs.push({
date: date,
time: time,
user: user && user !== '-' ? user : 'Неизвестно',
ip: ip,
method: method,
page: page,
statusCode: parseInt(statusCode),
filename: filename
});
}
} else {
// Старый метод парсинга для файлов без заголовка Fields
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] || '';

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) &&
page !== '/favicon.ico' && page !== '/') {

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

return logs;
}

// Получение уникальных посещений (пользователь + страница + дата)
function getUniqueVisits(logs) {
const uniqueMap = new Map();
const uniqueLogs = [];

logs.forEach(log => {
const key = `${log.date}_${log.user}_${log.page}`;
if (!uniqueMap.has(key)) {
uniqueMap.set(key, true);
uniqueLogs.push(log);
}
});

return uniqueLogs;
}

// Применение фильтров
[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() {
let logsToFilter = showUnique ? getUniqueVisits(allLogs) : allLogs;

filteredLogs = logsToFilter.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.data.datasets[0].label = showUnique ? 'Уникальные посещения' : 'Все посещения';
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);

let firstDayOfWeek = firstDayOfMonth.getDay();
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() {
handleDateClick(dateString);
});
}

calendar.appendChild(dayElement);
}

updateCalendarSelection();
}

function handleDateClick(dateString) {
if (isShiftPressed && selectedDateStart) {
// Выбор периода
selectedDateEnd = dateString;

// Убедимся, что start раньше end
if (new Date(selectedDateStart) > new Date(selectedDateEnd)) {
[selectedDateStart, selectedDateEnd] = [selectedDateEnd, selectedDateStart];
}

dateFrom.value = selectedDateStart;
dateTo.value = selectedDateEnd;
} else {
// Выбор одной даты
selectedDateStart = dateString;
selectedDateEnd = dateString;
dateFrom.value = dateString;
dateTo.value = dateString;
}

updateCalendarSelection();
applyFilters();
}

function updateCalendarSelection() {
// Сбрасываем все выделения
document.querySelectorAll('.calendar-day').forEach(day => {
day.classList.remove('selected', 'in-range');
});

if (selectedDateStart) {
const startDate = new Date(selectedDateStart);
const endDate = new Date(selectedDateEnd || selectedDateStart);

document.querySelectorAll('.calendar-day').forEach(day => {
const dateString = day.dataset.date;
if (!dateString) return;

const currentDate = new Date(dateString);

if (currentDate >= startDate && currentDate <= endDate) {
if (currentDate.getTime() === startDate.getTime() ||
currentDate.getTime() === endDate.getTime()) {
day.classList.add('selected');
} else {
day.classList.add('in-range');
}
}
});
}
}

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;
selectedDateStart = dateRange.min;
selectedDateEnd = 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;
}

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

.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;
position: relative;
}

.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.in-range {
background-color: #ffa07a;
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;
}

.stats-toggle {
margin-bottom: 15px;
display: flex;
gap: 10px;
}

.toggle-btn {
background-color: #e9ecef;
border: 1px solid #ced4da;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
}

.toggle-btn.active {
background-color: #3498db;
color: white;
border-color: #3498db;
}

.calendar-hint {
margin-top: 10px;
font-size: 14px;
color: #6c757d;
text-align: center;
}

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

.filters {
flex-direction: column;
}

.period-selector {
flex-direction: column;
}

.stats-toggle {
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="clearPath" 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>

<div class="stats-toggle">
<button id="showAllVisits" class="toggle-btn active">Все посещения</button>
<button id="showUniqueVisits" class="toggle-btn">Уникальные посещения</button>
</div>

<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 class="calendar-hint" id="calendarHint">
Кликните на дату для выбора одного дня. Зажмите Shift и кликните для выбора периода.
</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 clearPathBtn = document.getElementById('clearPath');
const showAllVisitsBtn = document.getElementById('showAllVisits');
const showUniqueVisitsBtn = document.getElementById('showUniqueVisits');
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 calendarHint = document.getElementById('calendarHint');
const prevMonthBtn = document.getElementById('prevMonth');
const nextMonthBtn = document.getElementById('nextMonth');

let allLogs = [];
let filteredLogs = [];
let dateRange = {};
let currentCalendarDate = new Date();
let selectedDateStart = null;
let selectedDateEnd = null;
let selectedFiles = [];
let showUnique = false;
let isShiftPressed = false;

// Отслеживаем нажатие Shift
document.addEventListener('keydown', function(e) {
if (e.key === 'Shift') {
isShiftPressed = true;
calendarHint.textContent = 'Shift нажат. Кликните на вторую дату для выбора периода.';
}
});

document.addEventListener('keyup', function(e) {
if (e.key === 'Shift') {
isShiftPressed = false;
calendarHint.textContent = 'Кликните на дату для выбора одного дня. Зажмите Shift и кликните для выбора периода.';
}
});

// Инициализация графиков
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',
}
}
}
}
);

// Переключение между всеми и уникальными посещениями
showAllVisitsBtn.addEventListener('click', function() {
showUnique = false;
showAllVisitsBtn.classList.add('active');
showUniqueVisitsBtn.classList.remove('active');
applyFilters();
});

showUniqueVisitsBtn.addEventListener('click', function() {
showUnique = true;
showAllVisitsBtn.classList.remove('active');
showUniqueVisitsBtn.classList.add('active');
applyFilters();
});

// Сохранение пути в localStorage
function savePathToStorage(path) {
try {
localStorage.setItem('iisLogsPath', path);
} catch (e) {
console.warn('Не удалось сохранить путь в localStorage:', e.message);
}
}

// Загрузка пути из localStorage
function loadPathFromStorage() {
try {
return localStorage.getItem('iisLogsPath');
} catch (e) {
console.warn('Не удалось загрузить путь из localStorage:', e.message);
return null;
}
}

// Очистка пути
clearPathBtn.addEventListener('click', function() {
try {
localStorage.removeItem('iisLogsPath');
fileInfoDiv.textContent = 'Путь очищен';
selectedFiles = [];
} catch (e) {
console.warn('Не удалось очистить путь:', e.message);
}
});

// Обработка выбора файлов
logFilesInput.addEventListener('change', function(e) {
selectedFiles = Array.from(e.target.files);
updateFileInfo();
if (selectedFiles.length > 0) {
savePathToStorage(`Файлы: ${selectedFiles.map(f => f.name).join(', ')}`);
}
});

// Обработка выбора папки
logFolderInput.addEventListener('change', function(e) {
selectedFiles = Array.from(e.target.files);
updateFileInfo();
if (selectedFiles.length > 0) {
savePathToStorage(`Папка с ${selectedFiles.length} файлами`);
}
});

// Обновление информации о выбранных файлах
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';
allLogs = [];

// Чтение файлов последовательно для избежания переполнения
processFilesSequentially(selectedFiles, 0)
.then(() => {
if (allLogs.length === 0) {
throw new Error('Не удалось распознать данные ни в одном файле');
}

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

fileInfoDiv.textContent += `. Успешно обработано записей: ${allLogs.length}`;
})
.catch(error => {
loadingDiv.style.display = 'none';
showError('Ошибка при обработке логов: ' + error.message);
});
});

// Последовательная обработка файлов
function processFilesSequentially(files, index) {
if (index >= files.length) {
return Promise.resolve();
}

const file = files[index];
return new Promise((resolve, reject) => {
const reader = new FileReader();

reader.onload = function(e) {
try {
const logs = parseLogFile(e.target.result, file.name);
if (logs.length > 0) {
allLogs = allLogs.concat(logs);
}
resolve(processFilesSequentially(files, index + 1));
} catch (error) {
console.warn(`Ошибка парсинга файла ${file.name}:`, error.message);
// Продолжаем обработку следующих файлов даже при ошибке
resolve(processFilesSequentially(files, index + 1));
}
};

reader.onerror = function() {
console.warn(`Ошибка чтения файла ${file.name}`);
// Продолжаем обработку следующих файлов даже при ошибке
resolve(processFilesSequentially(files, index + 1));
};

reader.readAsText(file);
});
}

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

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

// Пропускаем служебные строки
if (trimmedLine.startsWith('#')) continue;

try {
// Простой парсинг - ищем дату и основные поля
const parts = trimmedLine.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] || '';

// Ищем метод (GET/POST) после времени
let methodIndex = -1;
for (let j = dateIndex + 2; j < parts.length; j++) {
if (parts[j] === 'GET' || parts[j] === 'POST') {
methodIndex = j;
break;
}
}

if (methodIndex === -1) continue;

const method = parts[methodIndex];
const page = parts[methodIndex + 1] || '';

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

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

// Ищем IP клиента (формат xxx.xxx.xxx.xxx)
let ip = '';
for (let j = methodIndex + 2; j < parts.length; j++) {
if (parts[j].match(/^\d+\.\d+\.\d+\.\d+$/)) {
ip = parts[j];
break;
}
}

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

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

return logs;
}

// Получение уникальных посещений (пользователь + страница + дата)
function getUniqueVisits(logs) {
const uniqueMap = new Map();
const uniqueLogs = [];

logs.forEach(log => {
const key = `${log.date}_${log.user}_${log.page}`;
if (!uniqueMap.has(key)) {
uniqueMap.set(key, true);
uniqueLogs.push(log);
}
});

return uniqueLogs;
}

// Применение фильтров
[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() {
let logsToFilter = showUnique ? getUniqueVisits(allLogs) : allLogs;

filteredLogs = logsToFilter.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.data.datasets[0].label = showUnique ? 'Уникальные посещения' : 'Все посещения';
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);

let firstDayOfWeek = firstDayOfMonth.getDay();
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() {
handleDateClick(dateString);
});
}

calendar.appendChild(dayElement);
}

updateCalendarSelection();
}

function handleDateClick(dateString) {
if (isShiftPressed && selectedDateStart) {
// Выбор периода
selectedDateEnd = dateString;

// Убедимся, что start раньше end
if (new Date(selectedDateStart) > new Date(selectedDateEnd)) {
[selectedDateStart, selectedDateEnd] = [selectedDateEnd, selectedDateStart];
}

dateFrom.value = selectedDateStart;
dateTo.value = selectedDateEnd;
} else {
// Выбор одной даты
selectedDateStart = dateString;
selectedDateEnd = dateString;
dateFrom.value = dateString;
dateTo.value = dateString;
}

updateCalendarSelection();
applyFilters();
}

function updateCalendarSelection() {
// Сбрасываем все выделения
document.querySelectorAll('.calendar-day').forEach(day => {
day.classList.remove('selected', 'in-range');
});

if (selectedDateStart) {
const startDate = new Date(selectedDateStart);
const endDate = new Date(selectedDateEnd || selectedDateStart);

document.querySelectorAll('.calendar-day').forEach(day => {
const dateString = day.dataset.date;
if (!dateString) return;

const currentDate = new Date(dateString);

if (currentDate >= startDate && currentDate <= endDate) {
if (currentDate.getTime() === startDate.getTime() ||
currentDate.getTime() === endDate.getTime()) {
day.classList.add('selected');
} else {
day.classList.add('in-range');
}
}
});
}
}

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;
selectedDateStart = dateRange.min;
selectedDateEnd = dateRange.max;
}

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

// Показываем сохраненный путь при загрузке
const savedPath = loadPathFromStorage();
if (savedPath) {
fileInfoDiv.textContent = `Последний путь: ${savedPath}`;
}
});
</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;
}

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

.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;
position: relative;
}

.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.in-range {
background-color: #ffa07a;
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;
}

.stats-toggle {
margin-bottom: 15px;
display: flex;
gap: 10px;
}

.toggle-btn {
background-color: #e9ecef;
border: 1px solid #ced4da;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
}

.toggle-btn.active {
background-color: #3498db;
color: white;
border-color: #3498db;
}

.calendar-hint {
margin-top: 10px;
font-size: 14px;
color: #6c757d;
text-align: center;
}

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

.filters {
flex-direction: column;
}

.period-selector {
flex-direction: column;
}

.stats-toggle {
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="clearPath" 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>

<div class="stats-toggle">
<button id="showAllVisits" class="toggle-btn active">Все посещения</button>
<button id="showUniqueVisits" class="toggle-btn">Уникальные посещения</button>
</div>

<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 class="calendar-hint" id="calendarHint">
Кликните на дату для выбора одного дня. Зажмите Shift и кликните для выбора периода.
</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 clearPathBtn = document.getElementById('clearPath');
const showAllVisitsBtn = document.getElementById('showAllVisits');
const showUniqueVisitsBtn = document.getElementById('showUniqueVisits');
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 calendarHint = document.getElementById('calendarHint');
const prevMonthBtn = document.getElementById('prevMonth');
const nextMonthBtn = document.getElementById('nextMonth');

// Вместо хранения всех логов в памяти, будем хранить только агрегированные данные
let aggregatedData = {
dateCounts: {},
pageCounts: {},
userPageDateMap: {}, // Для уникальных посещений
allDates: new Set(),
minDate: null,
maxDate: null
};

let dateRange = {};
let currentCalendarDate = new Date();
let selectedDateStart = null;
let selectedDateEnd = null;
let selectedFiles = [];
let showUnique = false;
let isShiftPressed = false;
let processedFilesCount = 0;

// Отслеживаем нажатие Shift
document.addEventListener('keydown', function(e) {
if (e.key === 'Shift') {
isShiftPressed = true;
calendarHint.textContent = 'Shift нажат. Кликните на вторую дату для выбора периода.';
}
});

document.addEventListener('keyup', function(e) {
if (e.key === 'Shift') {
isShiftPressed = false;
calendarHint.textContent = 'Кликните на дату для выбора одного дня. Зажмите Shift и кликните для выбора периода.';
}
});

// Инициализация графиков
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',
}
}
}
}
);

// Переключение между всеми и уникальными посещениями
showAllVisitsBtn.addEventListener('click', function() {
showUnique = false;
showAllVisitsBtn.classList.add('active');
showUniqueVisitsBtn.classList.remove('active');
applyFilters();
});

showUniqueVisitsBtn.addEventListener('click', function() {
showUnique = true;
showAllVisitsBtn.classList.remove('active');
showUniqueVisitsBtn.classList.add('active');
applyFilters();
});

// Сохранение пути в localStorage
function savePathToStorage(path) {
try {
localStorage.setItem('iisLogsPath', path);
} catch (e) {
console.warn('Не удалось сохранить путь в localStorage:', e.message);
}
}

// Загрузка пути из localStorage
function loadPathFromStorage() {
try {
return localStorage.getItem('iisLogsPath');
} catch (e) {
console.warn('Не удалось загрузить путь из localStorage:', e.message);
return null;
}
}

// Очистка пути
clearPathBtn.addEventListener('click', function() {
try {
localStorage.removeItem('iisLogsPath');
fileInfoDiv.textContent = 'Путь очищен';
selectedFiles = [];
resetAggregatedData();
} catch (e) {
console.warn('Не удалось очистить путь:', e.message);
}
});

// Сброс агрегированных данных
function resetAggregatedData() {
aggregatedData = {
dateCounts: {},
pageCounts: {},
userPageDateMap: {},
allDates: new Set(),
minDate: null,
maxDate: null
};
statsTableBody.innerHTML = '';
updateCharts();
updatePopularPagesList();
updateCalendar();
}

// Обработка выбора файлов
logFilesInput.addEventListener('change', function(e) {
selectedFiles = Array.from(e.target.files);
updateFileInfo();
if (selectedFiles.length > 0) {
savePathToStorage(`Файлы: ${selectedFiles.map(f => f.name).join(', ')}`);
}
});

// Обработка выбора папки
logFolderInput.addEventListener('change', function(e) {
selectedFiles = Array.from(e.target.files);
updateFileInfo();
if (selectedFiles.length > 0) {
savePathToStorage(`Папка с ${selectedFiles.length} файлами`);
}
});

// Обновление информации о выбранных файлах
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';
resetAggregatedData();
processedFilesCount = 0;

// Обработка файлов последовательно
processFilesSequentially(selectedFiles, 0)
.then(() => {
if (aggregatedData.allDates.size === 0) {
throw new Error('Не удалось распознать данные ни в одном файле');
}

dateRange = {
min: aggregatedData.minDate,
max: aggregatedData.maxDate
};

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

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

// Последовательная обработка файлов
function processFilesSequentially(files, index) {
if (index >= files.length) {
return Promise.resolve();
}

const file = files[index];
return new Promise((resolve, reject) => {
const reader = new FileReader();

reader.onload = function(e) {
try {
processLogFile(e.target.result, file.name);
processedFilesCount++;
// Обновляем прогресс
fileInfoDiv.textContent = `Обработано файлов: ${processedFilesCount}/${files.length}`;
resolve(processFilesSequentially(files, index + 1));
} catch (error) {
console.warn(`Ошибка парсинга файла ${file.name}:`, error.message);
// Продолжаем обработку следующих файлов даже при ошибке
resolve(processFilesSequentially(files, index + 1));
}
};

reader.onerror = function() {
console.warn(`Ошибка чтения файла ${file.name}`);
// Продолжаем обработку следующих файлов даже при ошибке
resolve(processFilesSequentially(files, index + 1));
};

reader.readAsText(file);
});
}

// Обработка файла логов с немедленной агрегацией данных
function processLogFile(content, filename) {
const lines = content.split('\n');

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

// Пропускаем служебные строки
if (trimmedLine.startsWith('#')) continue;

try {
// Простой парсинг - ищем дату и основные поля
const parts = trimmedLine.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] || '';

// Ищем метод (GET/POST) после времени
let methodIndex = -1;
for (let j = dateIndex + 2; j < parts.length; j++) {
if (parts[j] === 'GET' || parts[j] === 'POST') {
methodIndex = j;
break;
}
}

if (methodIndex === -1) continue;

const method = parts[methodIndex];
const page = parts[methodIndex + 1] || '';

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

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

// Ищем IP клиента (формат xxx.xxx.xxx.xxx)
let ip = '';
for (let j = methodIndex + 2; j < parts.length; j++) {
if (parts[j].match(/^\d+\.\d+\.\d+\.\d+$/)) {
ip = parts[j];
break;
}
}

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

// Агрегируем данные вместо хранения всех логов
aggregateLogData({
date: date,
time: time,
user: user,
ip: ip,
method: method,
page: page,
statusCode: parseInt(statusCode),
filename: filename
});
}
} catch (e) {
// Пропускаем строки с ошибками
continue;
}
}
}

// Агрегация данных лога без хранения в памяти
function aggregateLogData(log) {
// Обновляем диапазон дат
aggregatedData.allDates.add(log.date);

if (!aggregatedData.minDate || log.date < aggregatedData.minDate) {
aggregatedData.minDate = log.date;
}
if (!aggregatedData.maxDate || log.date > aggregatedData.maxDate) {
aggregatedData.maxDate = log.date;
}

// Считаем посещения по датам
aggregatedData.dateCounts[log.date] = (aggregatedData.dateCounts[log.date] || 0) + 1;

// Считаем популярные страницы
aggregatedData.pageCounts[log.page] = (aggregatedData.pageCounts[log.page] || 0) + 1;

// Для уникальных посещений
const uniqueKey = `${log.date}_${log.user}_${log.page}`;
if (!aggregatedData.userPageDateMap[uniqueKey]) {
aggregatedData.userPageDateMap[uniqueKey] = log;
}
}

// Получение уникальных посещений из агрегированных данных
function getUniqueVisitsData() {
return Object.values(aggregatedData.userPageDateMap);
}

// Применение фильтров
[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() {
updateStatsTable();
updateCharts();
updatePopularPagesList();
}

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

// Для таблицы используем либо все данные, либо уникальные
let dataToShow;
if (showUnique) {
dataToShow = getUniqueVisitsData();
} else {
// В реальном приложении здесь нужно будет хранить выборку данных для таблицы
// или генерировать их на лету из агрегированных данных
dataToShow = Object.values(aggregatedData.userPageDateMap).slice(0, 1000); // Ограничиваем для производительности
}

// Применяем фильтры к данным для таблицы
const filteredData = dataToShow.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;
});

// Ограничиваем количество строк для производительности
const displayData = filteredData.slice(0, 1000);

displayData.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);
});

if (filteredData.length > 1000) {
const infoRow = document.createElement('tr');
const infoCell = document.createElement('td');
infoCell.colSpan = 7;
infoCell.textContent = `... и еще ${filteredData.length - 1000} записей`;
infoCell.style.textAlign = 'center';
infoCell.style.fontStyle = 'italic';
infoRow.appendChild(infoCell);
statsTableBody.appendChild(infoRow);
}
}

function updateCharts() {
// Обновление графика по датам с группировкой
const visitsByPeriod = {};

// Используем агрегированные данные для графиков
Object.keys(aggregatedData.dateCounts).forEach(date => {
const dateObj = new Date(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);

if (dateObj >= fromDate && dateObj <= toDate) {
let periodKey;

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

if (showUnique) {
// Для уникальных посещений нужно пересчитать
const uniqueCount = Object.keys(aggregatedData.userPageDateMap)
.filter(key => key.startsWith(date))
.length;
visitsByPeriod[periodKey] = (visitsByPeriod[periodKey] || 0) + uniqueCount;
} else {
visitsByPeriod[periodKey] = (visitsByPeriod[periodKey] || 0) + aggregatedData.dateCounts[date];
}
}
});

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

visitsByDateChart.data.labels = periods;
visitsByDateChart.data.datasets[0].data = visits;
visitsByDateChart.data.datasets[0].label = showUnique ? 'Уникальные посещения' : 'Все посещения';
visitsByDateChart.update();

// Обновление графика популярных страниц
const pageCounts = {};

// Фильтруем страницы по дате и другим критериям
Object.values(aggregatedData.userPageDateMap).forEach(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());

if (dateMatch && userMatch && pageMatch) {
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 = {};

// Фильтруем страницы по дате и другим критериям
Object.values(aggregatedData.userPageDateMap).forEach(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());

if (dateMatch && userMatch && pageMatch) {
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 = Array.from(aggregatedData.allDates).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);

let firstDayOfWeek = firstDayOfMonth.getDay();
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() {
handleDateClick(dateString);
});
}

calendar.appendChild(dayElement);
}

updateCalendarSelection();
}

function handleDateClick(dateString) {
if (isShiftPressed && selectedDateStart) {
// Выбор периода
selectedDateEnd = dateString;

// Убедимся, что start раньше end
if (new Date(selectedDateStart) > new Date(selectedDateEnd)) {
[selectedDateStart, selectedDateEnd] = [selectedDateEnd, selectedDateStart];
}

dateFrom.value = selectedDateStart;
dateTo.value = selectedDateEnd;
} else {
// Выбор одной даты
selectedDateStart = dateString;
selectedDateEnd = dateString;
dateFrom.value = dateString;
dateTo.value = dateString;
}

updateCalendarSelection();
applyFilters();
}

function updateCalendarSelection() {
// Сбрасываем все выделения
document.querySelectorAll('.calendar-day').forEach(day => {
day.classList.remove('selected', 'in-range');
});

if (selectedDateStart) {
const startDate = new Date(selectedDateStart);
const endDate = new Date(selectedDateEnd || selectedDateStart);

document.querySelectorAll('.calendar-day').forEach(day => {
const dateString = day.dataset.date;
if (!dateString) return;

const currentDate = new Date(dateString);

if (currentDate >= startDate && currentDate <= endDate) {
if (currentDate.getTime() === startDate.getTime() ||
currentDate.getTime() === endDate.getTime()) {
day.classList.add('selected');
} else {
day.classList.add('in-range');
}
}
});
}
}

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

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

// Показываем сохраненный путь при загрузке
const savedPath = loadPathFromStorage();
if (savedPath) {
fileInfoDiv.textContent = `Последний путь: ${savedPath}`;
}
});
</script>
</body>
</html>
Yyhh
Прикрепления:
Ррр
Прикрепления:
newfilerrrr.txt (54.7 Kb)
Ооорр
Рррр
Прикрепления:
newfile66666.txt (45.5 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;
}

.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;
}

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

.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;
position: relative;
}

.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.in-range {
background-color: #ffa07a;
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;
}

.stats-toggle {
margin-bottom: 15px;
display: flex;
gap: 10px;
}

.toggle-btn {
background-color: #e9ecef;
border: 1px solid #ced4da;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
}

.toggle-btn.active {
background-color: #3498db;
color: white;
border-color: #3498db;
}

.calendar-hint {
margin-top: 10px;
font-size: 14px;
color: #6c757d;
text-align: center;
}

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

.filters {
flex-direction: column;
}

.period-selector {
flex-direction: column;
}

.stats-toggle {
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="clearPath" 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>

<div class="stats-toggle">
<button id="showAllVisits" class="toggle-btn active">Все посещения</button>
<button id="showUniqueVisits" class="toggle-btn">Уникальные посещения</button>
</div>

<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 class="calendar-hint" id="calendarHint">
Кликните на дату для выбора одного дня. Зажмите Shift и кликните для выбора периода.
</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 clearPathBtn = document.getElementById('clearPath');
const showAllVisitsBtn = document.getElementById('showAllVisits');
const showUniqueVisitsBtn = document.getElementById('showUniqueVisits');
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 calendarHint = document.getElementById('calendarHint');
const prevMonthBtn = document.getElementById('prevMonth');
const nextMonthBtn = document.getElementById('nextMonth');

// Вместо хранения всех логов в памяти, будем хранить только агрегированные данные
let aggregatedData = {
dateCounts: {},
pageCounts: {},
userPageDateMap: {}, // Для уникальных посещений
allDates: new Set(),
minDate: null,
maxDate: null,
allLogs: [] // Храним ВСЕ логи для отображения в таблице
};

let dateRange = {};
let currentCalendarDate = new Date();
let selectedDateStart = null;
let selectedDateEnd = null;
let selectedFiles = [];
let showUnique = false;
let isShiftPressed = false;
let processedFilesCount = 0;

// Отслеживаем нажатие Shift
document.addEventListener('keydown', function(e) {
if (e.key === 'Shift') {
isShiftPressed = true;
calendarHint.textContent = 'Shift нажат. Кликните на вторую дату для выбора периода.';
}
});

document.addEventListener('keyup', function(e) {
if (e.key === 'Shift') {
isShiftPressed = false;
calendarHint.textContent = 'Кликните на дату для выбора одного дня. Зажмите Shift и кликните для выбора периода.';
}
});

// Инициализация графиков
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',
}
}
}
}
);

// Переключение между всеми и уникальными посещениями
showAllVisitsBtn.addEventListener('click', function() {
showUnique = false;
showAllVisitsBtn.classList.add('active');
showUniqueVisitsBtn.classList.remove('active');
applyFilters();
});

showUniqueVisitsBtn.addEventListener('click', function() {
showUnique = true;
showAllVisitsBtn.classList.remove('active');
showUniqueVisitsBtn.classList.add('active');
applyFilters();
});

// Сохранение пути в localStorage
function savePathToStorage(path) {
try {
localStorage.setItem('iisLogsPath', path);
} catch (e) {
console.warn('Не удалось сохранить путь в localStorage:', e.message);
}
}

// Загрузка пути из localStorage
function loadPathFromStorage() {
try {
return localStorage.getItem('iisLogsPath');
} catch (e) {
console.warn('Не удалось загрузить путь из localStorage:', e.message);
return null;
}
}

// Очистка пути
clearPathBtn.addEventListener('click', function() {
try {
localStorage.removeItem('iisLogsPath');
fileInfoDiv.textContent = 'Путь очищен';
selectedFiles = [];
resetAggregatedData();
} catch (e) {
console.warn('Не удалось очистить путь:', e.message);
}
});

// Сброс агрегированных данных
function resetAggregatedData() {
aggregatedData = {
dateCounts: {},
pageCounts: {},
userPageDateMap: {},
allDates: new Set(),
minDate: null,
maxDate: null,
allLogs: []
};
statsTableBody.innerHTML = '';
updateCharts();
updatePopularPagesList();
updateCalendar();
}

// Обработка выбора файлов
logFilesInput.addEventListener('change', function(e) {
selectedFiles = Array.from(e.target.files);
updateFileInfo();
if (selectedFiles.length > 0) {
savePathToStorage(`Файлы: ${selectedFiles.map(f => f.name).join(', ')}`);
}
});

// Обработка выбора папки
logFolderInput.addEventListener('change', function(e) {
selectedFiles = Array.from(e.target.files);
updateFileInfo();
if (selectedFiles.length > 0) {
savePathToStorage(`Папка с ${selectedFiles.length} файлами`);
}
});

// Обновление информации о выбранных файлах
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';
resetAggregatedData();
processedFilesCount = 0;

// Обработка файлов последовательно
processFilesSequentially(selectedFiles, 0)
.then(() => {
if (aggregatedData.allLogs.length === 0) {
throw new Error('Не удалось распознать данные ни в одном файле');
}

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

fileInfoDiv.textContent += `. Успешно обработано записей: ${aggregatedData.allLogs.length}`;
})
.catch(error => {
loadingDiv.style.display = 'none';
showError('Ошибка при обработке логов: ' + error.message);
});
});

// Последовательная обработка файлов
function processFilesSequentially(files, index) {
if (index >= files.length) {
return Promise.resolve();
}

const file = files[index];
return new Promise((resolve, reject) => {
const reader = new FileReader();

reader.onload = function(e) {
try {
const logs = parseLogFile(e.target.result, file.name);
if (logs.length > 0) {
// Агрегируем данные
logs.forEach(log => aggregateLogData(log));
}
processedFilesCount++;
fileInfoDiv.textContent = `Обработано файлов: ${processedFilesCount}/${files.length}`;
resolve(processFilesSequentially(files, index + 1));
} catch (error) {
console.warn(`Ошибка парсинга файла ${file.name}:`, error.message);
resolve(processFilesSequentially(files, index + 1));
}
};

reader.onerror = function() {
console.warn(`Ошибка чтения файла ${file.name}`);
resolve(processFilesSequentially(files, index + 1));
};

reader.readAsText(file);
});
}

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

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

// Пропускаем служебные строки
if (trimmedLine.startsWith('#')) continue;

try {
// Простой парсинг - ищем дату и основные поля
const parts = trimmedLine.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] || '';

// Ищем метод (GET/POST) после времени
let methodIndex = -1;
for (let j = dateIndex + 2; j < parts.length; j++) {
if (parts[j] === 'GET' || parts[j] === 'POST') {
methodIndex = j;
break;
}
}

if (methodIndex === -1) continue;

const method = parts[methodIndex];
const page = parts[methodIndex + 1] || '';

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

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

// Ищем IP клиента (формат xxx.xxx.xxx.xxx)
let ip = '';
for (let j = methodIndex + 2; j < parts.length; j++) {
if (parts[j].match(/^\d+\.\d+\.\d+\.\d+$/)) {
ip = parts[j];
break;
}
}

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

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

return logs;
}

// Агрегация данных лога
function aggregateLogData(log) {
// Сохраняем полный лог для таблицы
aggregatedData.allLogs.push(log);

// Обновляем диапазон дат
aggregatedData.allDates.add(log.date);

if (!aggregatedData.minDate || log.date < aggregatedData.minDate) {
aggregatedData.minDate = log.date;
}
if (!aggregatedData.maxDate || log.date > aggregatedData.maxDate) {
aggregatedData.maxDate = log.date;
}

// Считаем посещения по датам
aggregatedData.dateCounts[log.date] = (aggregatedData.dateCounts[log.date] || 0) + 1;

// Считаем популярные страницы
aggregatedData.pageCounts[log.page] = (aggregatedData.pageCounts[log.page] || 0) + 1;

// Для уникальных посещений
const uniqueKey = `${log.date}_${log.user}_${log.page}`;
if (!aggregatedData.userPageDateMap[uniqueKey]) {
aggregatedData.userPageDateMap[uniqueKey] = log;
}
}

// Получение уникальных посещений (пользователь + страница + дата)
function getUniqueVisits(logs) {
const uniqueMap = new Map();
const uniqueLogs = [];

logs.forEach(log => {
const key = `${log.date}_${log.user}_${log.page}`;
if (!uniqueMap.has(key)) {
uniqueMap.set(key, true);
uniqueLogs.push(log);
}
});

return uniqueLogs;
}

// Применение фильтров
[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() {
let logsToFilter = showUnique ? getUniqueVisits(aggregatedData.allLogs) : aggregatedData.allLogs;

const filteredLogs = logsToFilter.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(filteredLogs);
updateCharts(filteredLogs);
updatePopularPagesList(filteredLogs);
}

function updateStatsTable(filteredLogs) {
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(filteredLogs) {
// Обновление графика по датам с группировкой
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.data.datasets[0].label = showUnique ? 'Уникальные посещения' : 'Все посещения';
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(filteredLogs) {
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(aggregatedData.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);

let firstDayOfWeek = firstDayOfMonth.getDay();
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() {
handleDateClick(dateString);
});
}

calendar.appendChild(dayElement);
}

updateCalendarSelection();
}

function handleDateClick(dateString) {
if (isShiftPressed && selectedDateStart) {
// Выбор периода
selectedDateEnd = dateString;

// Убедимся, что start раньше end
if (new Date(selectedDateStart) > new Date(selectedDateEnd)) {
[selectedDateStart, selectedDateEnd] = [selectedDateEnd, selectedDateStart];
}

dateFrom.value = selectedDateStart;
dateTo.value = selectedDateEnd;
} else {
// Выбор одной даты
selectedDateStart = dateString;
selectedDateEnd = dateString;
dateFrom.value = dateString;
dateTo.value = dateString;
}

updateCalendarSelection();
applyFilters();
}

function updateCalendarSelection() {
// Сбрасываем все выделения
document.querySelectorAll('.calendar-day').forEach(day => {
day.classList.remove('selected', 'in-range');
});

if (selectedDateStart) {
const startDate = new Date(selectedDateStart);
const endDate = new Date(selectedDateEnd || selectedDateStart);

document.querySelectorAll('.calendar-day').forEach(day => {
const dateString = day.dataset.date;
if (!dateString) return;

const currentDate = new Date(dateString);

if (currentDate >= startDate && currentDate <= endDate) {
if (currentDate.getTime() === startDate.getTime() ||
currentDate.getTime() === endDate.getTime()) {
day.classList.add('selected');
} else {
day.classList.add('in-range');
}
}
});
}
}

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;
selectedDateStart = dateRange.min;
selectedDateEnd = dateRange.max;
}

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

// Показываем сохраненный путь при загрузке
const savedPath = loadPathFromStorage();
if (savedPath) {
fileInfoDiv.textContent = `Последний путь: ${savedPath}`;
}
});
</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;
}

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

.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;
position: relative;
}

.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.in-range {
background-color: #ffa07a;
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;
}

.stats-toggle {
margin-bottom: 15px;
display: flex;
gap: 10px;
}

.toggle-btn {
background-color: #e9ecef;
border: 1px solid #ced4da;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
}

.toggle-btn.active {
background-color: #3498db;
color: white;
border-color: #3498db;
}

.calendar-hint {
margin-top: 10px;
font-size: 14px;
color: #6c757d;
text-align: center;
}

.debug-section {
margin-top: 10px;
padding: 10px;
background-color: #fff3cd;
border-radius: 4px;
font-size: 12px;
display: none;
}

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

.filters {
flex-direction: column;
}

.period-selector {
flex-direction: column;
}

.stats-toggle {
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="clearPath" class="btn btn-secondary">Очистить путь</button>
<button id="debugInfo" class="btn btn-secondary">Отладочная информация</button>
<div id="fileInfo" class="file-info"></div>
<div id="debugSection" class="debug-section"></div>
<div id="loading" class="loading">Загрузка и анализ логов...</div>
<div id="error" class="error"></div>
</section>

<div class="stats-toggle">
<button id="showAllVisits" class="toggle-btn active">Все посещения</button>
<button id="showUniqueVisits" class="toggle-btn">Уникальные посещения</button>
</div>

<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 class="calendar-hint" id="calendarHint">
Кликните на дату для выбора одного дня. Зажмите Shift и кликните для выбора периода.
</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 clearPathBtn = document.getElementById('clearPath');
const debugInfoBtn = document.getElementById('debugInfo');
const showAllVisitsBtn = document.getElementById('showAllVisits');
const showUniqueVisitsBtn = document.getElementById('showUniqueVisits');
const loadingDiv = document.getElementById('loading');
const errorDiv = document.getElementById('error');
const fileInfoDiv = document.getElementById('fileInfo');
const debugSection = document.getElementById('debugSection');
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 calendarHint = document.getElementById('calendarHint');
const prevMonthBtn = document.getElementById('prevMonth');
const nextMonthBtn = document.getElementById('nextMonth');

// Храним все логи для детальной статистики (как в исходнике)
let allLogs = [];
let filteredLogs = [];
let dateRange = {};
let currentCalendarDate = new Date();
let selectedDateStart = null;
let selectedDateEnd = null;
let selectedFiles = [];
let showUnique = false;
let isShiftPressed = false;
let processedFilesCount = 0;

// Отслеживаем нажатие Shift
document.addEventListener('keydown', function(e) {
if (e.key === 'Shift') {
isShiftPressed = true;
calendarHint.textContent = 'Shift нажат. Кликните на вторую дату для выбора периода.';
}
});

document.addEventListener('keyup', function(e) {
if (e.key === 'Shift') {
isShiftPressed = false;
calendarHint.textContent = 'Кликните на дату для выбора одного дня. Зажмите Shift и кликните для выбора периода.';
}
});

// Инициализация графиков
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',
}
}
}
}
);

// Переключение между всеми и уникальными посещениями
showAllVisitsBtn.addEventListener('click', function() {
showUnique = false;
showAllVisitsBtn.classList.add('active');
showUniqueVisitsBtn.classList.remove('active');
applyFilters();
});

showUniqueVisitsBtn.addEventListener('click', function() {
showUnique = true;
showAllVisitsBtn.classList.remove('active');
showUniqueVisitsBtn.classList.add('active');
applyFilters();
});

// Отладочная информация
debugInfoBtn.addEventListener('click', function() {
debugSection.style.display = 'block';
debugSection.innerHTML = `
<strong>Отладочная информация:</strong><br>
Обработано файлов: ${processedFilesCount}<br>
Всего записей: ${allLogs.length}<br>
Уникальных дат: ${[...new Set(allLogs.map(log => log.date))].length}<br>
Диапазон дат: ${dateRange.min || 'нет'} - ${dateRange.max || 'нет'}<br>
Пример данных: ${JSON.stringify(allLogs.slice(0, 2))}
`;
});

// Сохранение пути в localStorage
function savePathToStorage(path) {
try {
localStorage.setItem('iisLogsPath', path);
} catch (e) {
console.warn('Не удалось сохранить путь в localStorage:', e.message);
}
}

// Загрузка пути из localStorage
function loadPathFromStorage() {
try {
return localStorage.getItem('iisLogsPath');
} catch (e) {
console.warn('Не удалось загрузить путь из localStorage:', e.message);
return null;
}
}

// Очистка пути
clearPathBtn.addEventListener('click', function() {
try {
localStorage.removeItem('iisLogsPath');
fileInfoDiv.textContent = 'Путь очищен';
selectedFiles = [];
resetData();
} catch (e) {
console.warn('Не удалось очистить путь:', e.message);
}
});

// Сброс данных
function resetData() {
allLogs = [];
filteredLogs = [];
processedFilesCount = 0;
statsTableBody.innerHTML = '';
updateCharts();
updatePopularPagesList();
updateCalendar();
debugSection.style.display = 'none';
}

// Обработка выбора файлов
logFilesInput.addEventListener('change', function(e) {
selectedFiles = Array.from(e.target.files);
updateFileInfo();
if (selectedFiles.length > 0) {
savePathToStorage(`Файлы: ${selectedFiles.map(f => f.name).join(', ')}`);
}
});

// Обработка выбора папки
logFolderInput.addEventListener('change', function(e) {
selectedFiles = Array.from(e.target.files);
updateFileInfo();
if (selectedFiles.length > 0) {
savePathToStorage(`Папка с ${selectedFiles.length} файлами`);
}
});

// Обновление информации о выбранных файлах
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';
debugSection.style.display = 'none';
resetData();
processedFilesCount = 0;

// Обработка файлов последовательно
processFilesSequentially(selectedFiles, 0)
.then(() => {
if (allLogs.length === 0) {
throw new Error('Не удалось распознать данные ни в одном файле');
}

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

fileInfoDiv.textContent = `Успешно обработано файлов: ${processedFilesCount}/${selectedFiles.length}, записей: ${allLogs.length}`;
})
.catch(error => {
loadingDiv.style.display = 'none';
showError('Ошибка при обработке логов: ' + error.message);
});
});

// Последовательная обработка файлов
function processFilesSequentially(files, index) {
if (index >= files.length) {
return Promise.resolve();
}

const file = files[index];
return new Promise((resolve, reject) => {
const reader = new FileReader();

reader.onload = function(e) {
try {
const logs = parseLogFile(e.target.result, file.name);
if (logs.length > 0) {
allLogs = allLogs.concat(logs);
console.log(`Файл ${file.name}: обработано ${logs.length} записей`);
} else {
console.warn(`Файл ${file.name}: не найдено валидных записей`);
}

processedFilesCount++;
fileInfoDiv.textContent = `Обработано файлов: ${processedFilesCount}/${files.length}, записей: ${allLogs.length}`;
resolve(processFilesSequentially(files, index + 1));
} catch (error) {
console.warn(`Ошибка парсинга файла ${file.name}:`, error.message);
resolve(processFilesSequentially(files, index + 1));
}
};

reader.onerror = function() {
console.warn(`Ошибка чтения файла ${file.name}`);
resolve(processFilesSequentially(files, index + 1));
};

reader.readAsText(file);
});
}

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

// Получаем дату из заголовка файла (#Date:)
let fileDate = '';
for (const line of lines) {
if (line.startsWith('#Date:')) {
const dateMatch = line.match(/#Date:\s*(\d{4}-\d{2}-\d{2})/);
if (dateMatch) {
fileDate = dateMatch[1];
break;
}
}
}

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

// Пропускаем служебные строки
if (trimmedLine.startsWith('#')) continue;

try {
// Простой парсинг - ищем дату и основные поля
const parts = trimmedLine.split(/\s+/);

// Ищем дату в формате ГГГГ-ММ-ДД (из самой строки)
let dateIndex = -1;
let logDate = fileDate; // Используем дату из заголовка по умолчанию

for (let j = 0; j < parts.length; j++) {
if (parts[j].match(/^\d{4}-\d{2}-\d{2}$/)) {
dateIndex = j;
logDate = parts[j];
break;
}
}

if (dateIndex === -1 && !fileDate) continue;

const time = parts[dateIndex + 1] || '';

// Ищем метод (GET/POST) после времени
let methodIndex = -1;
for (let j = dateIndex + 2; j < parts.length; j++) {
if (parts[j] === 'GET' || parts[j] === 'POST') {
methodIndex = j;
break;
}
}

if (methodIndex === -1) continue;

const method = parts[methodIndex];
const page = parts[methodIndex + 1] || '';

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

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

// Ищем IP клиента (формат xxx.xxx.xxx.xxx)
let ip = '';
for (let j = methodIndex + 2; j < parts.length; j++) {
if (parts[j].match(/^\d+\.\d+\.\d+\.\d+$/)) {
ip = parts[j];
break;
}
}

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

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

return logs;
}

// Получение уникальных посещений (пользователь + страница + дата)
function getUniqueVisits(logs) {
const uniqueMap = new Map();
const uniqueLogs = [];

logs.forEach(log => {
const key = `${log.date}_${log.user}_${log.page}`;
if (!uniqueMap.has(key)) {
uniqueMap.set(key, true);
uniqueLogs.push(log);
}
});

return uniqueLogs;
}

// Применение фильтров
[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() {
let logsToFilter = showUnique ? getUniqueVisits(allLogs) : allLogs;

filteredLogs = logsToFilter.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.data.datasets[0].label = showUnique ? 'Уникальные посещения' : 'Все посещения';
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);

let firstDayOfWeek = firstDayOfMonth.getDay();
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() {
handleDateClick(dateString);
});
}

calendar.appendChild(dayElement);
}

updateCalendarSelection();
}

function handleDateClick(dateString) {
if (isShiftPressed && selectedDateStart) {
// Выбор периода
selectedDateEnd = dateString;

// Убедимся, что start раньше end
if (new Date(selectedDateStart) > new Date(selectedDateEnd)) {
[selectedDateStart, selectedDateEnd] = [selectedDateEnd, selectedDateStart];
}

dateFrom.value = selectedDateStart;
dateTo.value = selectedDateEnd;
} else {
// Выбор одной даты
selectedDateStart = dateString;
selectedDateEnd = dateString;
dateFrom.value = dateString;
dateTo.value = dateString;
}

updateCalendarSelection();
applyFilters();
}

function updateCalendarSelection() {
// Сбрасываем все выделения
document.querySelectorAll('.calendar-day').forEach(day => {
day.classList.remove('selected', 'in-range');
});

if (selectedDateStart) {
const startDate = new Date(selectedDateStart);
const endDate = new Date(selectedDateEnd || selectedDateStart);

document.querySelectorAll('.calendar-day').forEach(day => {
const dateString = day.dataset.date;
if (!dateString) return;

const currentDate = new Date(dateString);

if (currentDate >= startDate && currentDate <= endDate) {
if (currentDate.getTime() === startDate.getTime() ||
currentDate.getTime() === endDate.getTime()) {
day.classList.add('selected');
} else {
day.classList.add('in-range');
}
}
});
}
}

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;
selectedDateStart = dateRange.min;
selectedDateEnd = dateRange.max;
}

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

// Показываем сохраненный путь при загрузке
const savedPath = loadPathFromStorage();
if (savedPath) {
fileInfoDiv.textContent = `Последний путь: ${savedPath}`;
}
});
</script>
</body>
</html>
Рррр
Прикрепления:
rayprom.noext (52.4 Kb)
Рррр
Прикрепления:
khdop.noext (52.3 Kb)
Рро
Прикрепления:
ddd.noext (54.4 Kb)
Оорр
Прикрепления:
llrpa.noext (60.8 Kb)
Поиск:
Новый ответ
Имя:
Текст сообщения: