# Руководство по API для фронтенда

## Обзор изменений

После рефакторинга старый `ParserController` был разделен на три специализированных контроллера для лучшей организации и масштабируемости:

-   **`MarketItemController`** (`/api/items`) - для получения данных
-   **`ParserAdminController`** (`/api/admin/parsers`) - для управления парсерами
-   **`HealthCheckController`** (`/api/health`) - для мониторинга системы

---

## 📊 MarketItemController - Получение данных

**Базовый URL**: `/api/items`

### 1. Получение списка новостей с фильтрацией и пагинацией

```http
GET /api/items
```

**Параметры запроса:**

-   `sourceName` (optional) - фильтр по источнику (например: "Kursiv Media", "Kapital.kz", "LSM.kz", "РБК", "Ведомости")
-   `startDate` (optional) - начальная дата в формате ISO (например: "2024-01-01T00:00:00")
-   `endDate` (optional) - конечная дата в формате ISO (например: "2024-12-31T23:59:59")
-   `page` (optional) - номер страницы (по умолчанию: 0)
-   `size` (optional) - размер страницы (по умолчанию: 20)
-   `sort` (optional) - сортировка (по умолчанию: "publishedAt,desc")

**Примеры запросов:**

```javascript
// Получить все новости
fetch('/api/items');

// Получить новости с пагинацией
fetch('/api/items?page=0&size=10');

// Получить новости от конкретного источника
fetch('/api/items?sourceName=Kursiv Media');

// Получить новости за последние 7 дней
const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
fetch(`/api/items?startDate=${weekAgo}`);

// Получить новости с сортировкой по дате (старые сначала)
fetch('/api/items?sort=publishedAt,asc');
```

**Ответ:**

```json
{
    "success": true,
    "message": "Записи получены успешно",
    "data": {
        "content": [
            {
                "id": "64f1a2b3c4d5e6f7g8h9i0j1",
                "sourceName": "Kursiv Media",
                "url": "https://kursiv.media/news/example",
                "title": "Заголовок новости",
                "publishedAt": "2024-01-15T10:30:00",
                "addedAt": "2024-01-15T10:35:00",
                "rawText": "Текст новости без HTML тегов",
                "hash": "abc123def456...",
                "category": "Бизнес",
                "analytics": {
                    "summary": null,
                    "sentiment": null,
                    "tags": [],
                    "entities": {}
                }
            }
        ],
        "pageable": {
            "sort": {
                "sorted": true,
                "unsorted": false
            },
            "pageNumber": 0,
            "pageSize": 20,
            "offset": 0,
            "paged": true,
            "unpaged": false
        },
        "totalElements": 150,
        "totalPages": 8,
        "last": false,
        "first": true,
        "numberOfElements": 20,
        "size": 20,
        "number": 0,
        "empty": false
    }
}
```

### 2. Получение новости по ID

```http
GET /api/items/{id}
```

**Пример:**

```javascript
fetch('/api/items/64f1a2b3c4d5e6f7g8h9i0j1');
```

**Ответ:**

```json
{
    "success": false,
    "message": "Функция поиска по ID пока не реализована"
}
```

### 3. Получение статистики

```http
GET /api/items/stats
```

**Ответ:**

```json
{
    "success": true,
    "message": "Статистика получена успешно",
    "data": {
        "totalItems": 1250,
        "sourceStatistics": {
            "Kursiv Media": 300,
            "Kapital.kz": 250,
            "LSM.kz": 200,
            "РБК": 300,
            "Ведомости": 200
        },
        "lastUpdate": "2024-01-15T10:35:00"
    }
}
```

### 4. Получение списка источников (sourceName)

```http
GET /api/items/sources
```

**Описание:**
Возвращает список всех уникальных источников новостей, которые есть в базе данных. Этот эндпоинт полезен для:

-   Построения фильтров по источникам
-   Отображения списка доступных источников в UI
-   Динамического создания выпадающих списков

**Ответ:**

```json
{
    "success": true,
    "message": "Источники получены успешно",
    "data": ["Kursiv (Бизнес/экономика)", "Kapital.kz (Бизнес)", "LSM.kz", "РБК", "Ведомости"]
}
```

**Примеры использования:**

```javascript
// Получить список всех источников
const response = await fetch('/api/items/sources');
const result = await response.json();

if (result.success) {
    const sources = result.data;
    console.log('Доступные источники:', sources);
    // sources = ["Kursiv (Бизнес/экономика)", "Kapital.kz (Бизнес)", "LSM.kz", "РБК", "Ведомости"]
}

// Использование в React для создания фильтра
function SourceFilter({ onSourceChange }) {
    const [sources, setSources] = useState([]);

    useEffect(() => {
        fetch('/api/items/sources')
            .then((res) => res.json())
            .then((data) => {
                if (data.success) {
                    setSources(data.data);
                }
            });
    }, []);

    return (
        <select onChange={(e) => onSourceChange(e.target.value)}>
            <option value="">Все источники</option>
            {sources.map((source) => (
                <option key={source} value={source}>
                    {source}
                </option>
            ))}
        </select>
    );
}

// Использование в Vue.js
export default {
    data() {
        return {
            sources: [],
            selectedSource: ''
        };
    },
    async mounted() {
        const response = await fetch('/api/items/sources');
        const result = await response.json();
        if (result.success) {
            this.sources = result.data;
        }
    }
};
```

**Важные замечания:**

-   Список источников формируется на основе реальных данных в базе
-   Если в базе нет данных от какого-то источника, он не будет включен в список
-   Названия источников точно соответствуют тем, что используются в поле `sourceName` при фильтрации
-   Порядок источников может изменяться в зависимости от того, как MongoDB возвращает уникальные значения

### 5. Получение новостей по конкретному источнику

```http
GET /api/items/source/{sourceName}
```

**Пример:**

```javascript
fetch('/api/items/source/Kursiv Media?page=0&size=10');
```

**Ответ:** Аналогичен ответу от `/api/items`, но только с новостями от указанного источника.

---

## ⚙️ ParserAdminController - Управление парсерами

**Базовый URL**: `/api/admin/parsers`

> ⚠️ **Важно**: Эти эндпоинты предназначены для административных функций и могут потребовать аутентификации в будущем.

### 1. Запуск конкретного парсера

```http
POST /api/admin/parsers/parse/{sourceName}
```

**Доступные источники:**

-   `kursiv`
-   `kapital`
-   `lsm`
-   `rbc`
-   `vedomosti`

**Пример:**

```javascript
// Запустить парсер Kursiv
fetch('/api/admin/parsers/parse/kursiv', { method: 'POST' });

// Запустить парсер РБК
fetch('/api/admin/parsers/parse/rbc', { method: 'POST' });
```

**Ответ:**

```json
{
    "success": true,
    "message": "Парсинг kursiv завершен",
    "data": {
        "source": "kursiv",
        "processedItems": 15,
        "totalItemsInDb": 1265,
        "status": "completed"
    }
}
```

### 2. Запуск всех парсеров

```http
POST /api/admin/parsers/parse/all
```

**Пример:**

```javascript
fetch('/api/admin/parsers/parse/all', { method: 'POST' });
```

**Ответ:**

```json
{
    "success": true,
    "message": "Парсинг всех источников завершен",
    "data": [
        {
            "source": "kursiv",
            "processedItems": 12,
            "totalItemsInDb": 1277,
            "status": "completed"
        },
        {
            "source": "kapital",
            "processedItems": 8,
            "totalItemsInDb": 1285,
            "status": "completed"
        },
        {
            "source": "lsm",
            "processedItems": 5,
            "totalItemsInDb": 1290,
            "status": "completed"
        },
        {
            "source": "rbc",
            "processedItems": 20,
            "totalItemsInDb": 1310,
            "status": "completed"
        },
        {
            "source": "vedomosti",
            "processedItems": 7,
            "totalItemsInDb": 1317,
            "status": "completed"
        }
    ]
}
```

### 3. Получение списка доступных парсеров

```http
GET /api/admin/parsers
```

**Ответ:**

```json
{
    "success": true,
    "message": "Список парсеров получен",
    "data": ["kapital", "kursiv", "lsm", "rbc", "vedomosti"]
}
```

### 4. Получение информации о парсерах

```http
GET /api/admin/parsers/info
```

**Ответ:**

```json
{
    "success": true,
    "message": "Информация о парсерах получена",
    "data": {
        "totalParsers": 5,
        "availableParsers": ["kapital", "kursiv", "lsm", "rbc", "vedomosti"],
        "totalItems": 1317,
        "sourceStatistics": {
            "Kursiv Media": 300,
            "Kapital.kz": 250,
            "LSM.kz": 200,
            "РБК": 300,
            "Ведомости": 200
        }
    }
}
```

### 5. Проверка существования парсера

```http
GET /api/admin/parsers/exists/{sourceName}
```

**Пример:**

```javascript
fetch('/api/admin/parsers/exists/kursiv');
```

**Ответ:**

```json
{
    "success": true,
    "message": "Проверка завершена",
    "data": true
}
```

---

## 🔍 HealthCheckController - Мониторинг системы

**Базовый URL**: `/api/health`

### 1. Базовая проверка состояния

```http
GET /api/health
```

**Ответ:**

```json
{
    "success": true,
    "message": "Сервис работает",
    "data": {
        "status": "UP",
        "service": "RSS Parser System",
        "timestamp": 1705312500000,
        "details": "Enabled - runs every 30 minutes"
    }
}
```

### 2. Проверка подключения к MongoDB

```http
GET /api/health/mongodb
```

**Ответ (успех):**

```json
{
    "success": true,
    "message": "MongoDB подключение успешно",
    "data": {
        "status": "UP",
        "service": "MongoDB подключение успешно",
        "timestamp": 1705312500000,
        "details": "Всего записей: 1317"
    }
}
```

**Ответ (ошибка):**

```json
{
    "success": false,
    "message": "Ошибка подключения к MongoDB",
    "data": {
        "status": "DOWN",
        "service": "Ошибка подключения к MongoDB: Connection refused",
        "timestamp": 1705312500000,
        "suggestion": "Проверьте учетные данные в application.properties"
    }
}
```

### 3. Информация о планировщике

```http
GET /api/health/scheduler
```

**Ответ:**

```json
{
    "success": true,
    "message": "Информация о планировщике получена",
    "data": {
        "schedulerEnabled": true,
        "cronExpression": "0 0/30 * * * ?",
        "description": "Запуск каждые 30 минут (в 0 и 30 минут каждого часа)",
        "nextRun": "Следующий запуск будет в ближайшие 0 или 30 минут часа",
        "activeParsers": ["kapital", "kursiv", "lsm", "rbc", "vedomosti"],
        "totalParsers": 5,
        "sources": ["Kursiv Media", "Kapital.kz", "LSM.kz", "РБК", "Ведомости"]
    }
}
```

### 4. Общая информация о системе

```http
GET /api/health/system
```

**Ответ:**

```json
{
    "success": true,
    "message": "Информация о системе получена",
    "data": {
        "parsers": {
            "total": 5,
            "available": ["kapital", "kursiv", "lsm", "rbc", "vedomosti"]
        },
        "data": {
            "totalItems": 1317,
            "sourceStatistics": {
                "Kursiv Media": 300,
                "Kapital.kz": 250,
                "LSM.kz": 200,
                "РБК": 300,
                "Ведомости": 200
            }
        },
        "system": {
            "javaVersion": "17.0.2",
            "osName": "Linux",
            "osVersion": "5.4.0-74-generic",
            "uptime": 1705312500000
        }
    }
}
```

### 5. Проверка состояния парсеров

```http
GET /api/health/parsers
```

**Ответ:**

```json
{
    "success": true,
    "message": "Проверка парсеров завершена",
    "data": {
        "totalParsers": 5,
        "availableParsers": ["kapital", "kursiv", "lsm", "rbc", "vedomosti"],
        "status": "UP",
        "message": "Все парсеры доступны"
    }
}
```

---

## 🚀 Примеры использования для фронтенда

### React/JavaScript примеры

```javascript
// Сервис для работы с API
class NewsApiService {
    constructor(baseUrl = '') {
        this.baseUrl = baseUrl;
    }

    // Получить новости с фильтрацией
    async getNews(filters = {}) {
        const params = new URLSearchParams();

        if (filters.sourceName) params.append('sourceName', filters.sourceName);
        if (filters.startDate) params.append('startDate', filters.startDate);
        if (filters.endDate) params.append('endDate', filters.endDate);
        if (filters.page !== undefined) params.append('page', filters.page);
        if (filters.size) params.append('size', filters.size);
        if (filters.sort) params.append('sort', filters.sort);

        const response = await fetch(`${this.baseUrl}/api/items?${params}`);
        return response.json();
    }

    // Получить статистику
    async getStats() {
        const response = await fetch(`${this.baseUrl}/api/items/stats`);
        return response.json();
    }

    // Получить источники
    async getSources() {
        const response = await fetch(`${this.baseUrl}/api/items/sources`);
        return response.json();
    }

    // Получить источники с обработкой ошибок
    async getSourcesWithErrorHandling() {
        try {
            const response = await fetch(`${this.baseUrl}/api/items/sources`);
            const result = await response.json();

            if (!result.success) {
                throw new Error(result.message || 'Ошибка при получении источников');
            }

            return result.data; // Возвращаем только массив источников
        } catch (error) {
            console.error('Ошибка при получении источников:', error);
            return []; // Возвращаем пустой массив в случае ошибки
        }
    }

    // Запустить парсер (админ функция)
    async runParser(sourceName) {
        const response = await fetch(`${this.baseUrl}/api/admin/parsers/parse/${sourceName}`, {
            method: 'POST'
        });
        return response.json();
    }

    // Запустить все парсеры (админ функция)
    async runAllParsers() {
        const response = await fetch(`${this.baseUrl}/api/admin/parsers/parse/all`, {
            method: 'POST'
        });
        return response.json();
    }

    // Проверить состояние системы
    async getHealthStatus() {
        const response = await fetch(`${this.baseUrl}/api/health`);
        return response.json();
    }
}

// Использование
const apiService = new NewsApiService();

// Получить последние новости
const news = await apiService.getNews({
    page: 0,
    size: 20,
    sort: 'publishedAt,desc'
});

// Получить новости от конкретного источника
const kursivNews = await apiService.getNews({
    sourceName: 'Kursiv Media',
    page: 0,
    size: 10
});

// Получить новости за последнюю неделю
const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
const recentNews = await apiService.getNews({
    startDate: weekAgo,
    sort: 'publishedAt,desc'
});

// Получить список источников для фильтра
const sources = await apiService.getSourcesWithErrorHandling();
console.log('Доступные источники:', sources);

// Получить новости от конкретного источника
if (sources.length > 0) {
    const newsFromFirstSource = await apiService.getNews({
        sourceName: sources[0],
        page: 0,
        size: 10
    });
}
```

### Vue.js примеры

```javascript
// composable для работы с API
export function useNewsApi() {
    const baseUrl = '';

    const getNews = async (filters = {}) => {
        const params = new URLSearchParams();
        Object.entries(filters).forEach(([key, value]) => {
            if (value !== undefined && value !== null) {
                params.append(key, value);
            }
        });

        const response = await fetch(`${baseUrl}/api/items?${params}`);
        return response.json();
    };

    const getStats = async () => {
        const response = await fetch(`${baseUrl}/api/items/stats`);
        return response.json();
    };

    const getSources = async () => {
        const response = await fetch(`${baseUrl}/api/items/sources`);
        return response.json();
    };

    const getSourcesList = async () => {
        try {
            const response = await fetch(`${baseUrl}/api/items/sources`);
            const result = await response.json();
            return result.success ? result.data : [];
        } catch (error) {
            console.error('Ошибка при получении источников:', error);
            return [];
        }
    };

    return {
        getNews,
        getStats,
        getSources,
        getSourcesList
    };
}
```

---

## 🎯 Практические примеры использования эндпоинта источников

### 1. Создание динамического фильтра источников

```javascript
// React компонент для фильтрации по источникам
import React, { useState, useEffect } from 'react';

function NewsFilter({ onFilterChange }) {
    const [sources, setSources] = useState([]);
    const [selectedSource, setSelectedSource] = useState('');

    useEffect(() => {
        // Загружаем список источников при монтировании компонента
        fetch('/api/items/sources')
            .then((res) => res.json())
            .then((data) => {
                if (data.success) {
                    setSources(data.data);
                }
            })
            .catch((err) => console.error('Ошибка загрузки источников:', err));
    }, []);

    const handleSourceChange = (source) => {
        setSelectedSource(source);
        onFilterChange({ sourceName: source || undefined });
    };

    return (
        <div className="news-filter">
            <label htmlFor="source-filter">Фильтр по источнику:</label>
            <select id="source-filter" value={selectedSource} onChange={(e) => handleSourceChange(e.target.value)}>
                <option value="">Все источники</option>
                {sources.map((source) => (
                    <option key={source} value={source}>
                        {source}
                    </option>
                ))}
            </select>
        </div>
    );
}

export default NewsFilter;
```

### 2. Отображение статистики по источникам

```javascript
// Vue.js компонент для отображения статистики
<template>
  <div class="sources-stats">
    <h3>Статистика по источникам</h3>
    <div v-if="loading" class="loading">Загрузка...</div>
    <div v-else class="stats-grid">
      <div
        v-for="source in sources"
        :key="source"
        class="stat-item"
        @click="filterBySource(source)"
      >
        <span class="source-name">{{ source }}</span>
        <span class="source-count">{{ getSourceCount(source) }}</span>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      sources: [],
      loading: true,
      stats: {}
    }
  },
  async mounted() {
    await this.loadSources();
    await this.loadStats();
    this.loading = false;
  },
  methods: {
    async loadSources() {
      try {
        const response = await fetch('/api/items/sources');
        const result = await response.json();
        if (result.success) {
          this.sources = result.data;
        }
      } catch (error) {
        console.error('Ошибка загрузки источников:', error);
      }
    },
    async loadStats() {
      try {
        const response = await fetch('/api/items/stats');
        const result = await response.json();
        if (result.success) {
          this.stats = result.data.sourceStatistics;
        }
      } catch (error) {
        console.error('Ошибка загрузки статистики:', error);
      }
    },
    getSourceCount(source) {
      return this.stats[source] || 0;
    },
    filterBySource(source) {
      this.$emit('filter-change', { sourceName: source });
    }
  }
}
</script>
```

### 3. Автодополнение для поиска по источникам

```javascript
// Angular сервис для автодополнения
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, BehaviorSubject } from 'rxjs';
import { map, catchError } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class SourceService {
  private sourcesSubject = new BehaviorSubject<string[]>([]);
  public sources$ = this.sourcesSubject.asObservable();

  constructor(private http: HttpClient) {
    this.loadSources();
  }

  private loadSources(): void {
    this.http.get<any>('/api/items/sources')
      .pipe(
        map(response => response.success ? response.data : []),
        catchError(error => {
          console.error('Ошибка загрузки источников:', error);
          return [];
        })
      )
      .subscribe(sources => {
        this.sourcesSubject.next(sources);
      });
  }

  searchSources(query: string): Observable<string[]> {
    return this.sources$.pipe(
      map(sources =>
        sources.filter(source =>
          source.toLowerCase().includes(query.toLowerCase())
        )
      )
    );
  }

  getSources(): Observable<string[]> {
    return this.sources$;
  }
}

// Компонент автодополнения
@Component({
  selector: 'app-source-autocomplete',
  template: `
    <div class="autocomplete">
      <input
        [(ngModel)]="searchQuery"
        (input)="onSearch($event.target.value)"
        placeholder="Поиск источника..."
        class="search-input"
      />
      <div *ngIf="filteredSources.length > 0" class="suggestions">
        <div
          *ngFor="let source of filteredSources"
          (click)="selectSource(source)"
          class="suggestion-item"
        >
          {{ source }}
        </div>
      </div>
    </div>
  `
})
export class SourceAutocompleteComponent {
  searchQuery = '';
  filteredSources: string[] = [];

  constructor(private sourceService: SourceService) {}

  onSearch(query: string): void {
    if (query.length > 0) {
      this.sourceService.searchSources(query)
        .subscribe(sources => {
          this.filteredSources = sources;
        });
    } else {
      this.filteredSources = [];
    }
  }

  selectSource(source: string): void {
    this.searchQuery = source;
    this.filteredSources = [];
    // Эмитим событие выбора источника
  }
}
```

### 4. Кэширование списка источников

```javascript
// Сервис с кэшированием для React
class SourceCacheService {
    constructor() {
        this.cache = null;
        this.cacheTime = null;
        this.cacheTimeout = 5 * 60 * 1000; // 5 минут
    }

    async getSources() {
        // Проверяем, есть ли актуальный кэш
        if (this.cache && this.cacheTime && Date.now() - this.cacheTime < this.cacheTimeout) {
            return this.cache;
        }

        try {
            const response = await fetch('/api/items/sources');
            const result = await response.json();

            if (result.success) {
                this.cache = result.data;
                this.cacheTime = Date.now();
                return result.data;
            } else {
                throw new Error(result.message);
            }
        } catch (error) {
            console.error('Ошибка загрузки источников:', error);
            // Возвращаем кэш, если есть, даже если он устарел
            return this.cache || [];
        }
    }

    clearCache() {
        this.cache = null;
        this.cacheTime = null;
    }
}

// Использование в React хуке
function useSources() {
    const [sources, setSources] = useState([]);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);

    useEffect(() => {
        const sourceService = new SourceCacheService();

        sourceService
            .getSources()
            .then((data) => {
                setSources(data);
                setLoading(false);
            })
            .catch((err) => {
                setError(err.message);
                setLoading(false);
            });
    }, []);

    return { sources, loading, error };
}
```

---

## 📝 Важные замечания

1. **Пагинация**: Все эндпоинты для получения данных поддерживают пагинацию через параметры `page` и `size`.

2. **Сортировка**: По умолчанию новости сортируются по дате публикации (новые сначала). Можно изменить через параметр `sort`.

3. **Фильтрация**: Поддерживается фильтрация по источнику и диапазону дат.

4. **Формат дат**: Используйте ISO 8601 формат для дат (например: "2024-01-15T10:30:00").

5. **Обработка ошибок**: Все ответы содержат поле `success` для проверки успешности операции.

6. **Административные функции**: Эндпоинты `/api/admin/parsers/*` предназначены для административных функций.

7. **Мониторинг**: Используйте эндпоинты `/api/health/*` для проверки состояния системы.

---

## 🔄 Миграция с старого API

Если у вас был код, использующий старый `ParserController`, вот соответствие эндпоинтов:

| Старый эндпоинт                    | Новый эндпоинт                            |
| ---------------------------------- | ----------------------------------------- |
| `POST /api/parser/parse/kursiv`    | `POST /api/admin/parsers/parse/kursiv`    |
| `POST /api/parser/parse/kapital`   | `POST /api/admin/parsers/parse/kapital`   |
| `POST /api/parser/parse/lsm`       | `POST /api/admin/parsers/parse/lsm`       |
| `POST /api/parser/parse/rbc`       | `POST /api/admin/parsers/parse/rbc`       |
| `POST /api/parser/parse/vedomosti` | `POST /api/admin/parsers/parse/vedomosti` |
| `POST /api/parser/parse/all`       | `POST /api/admin/parsers/parse/all`       |
| `GET /api/parser/items`            | `GET /api/items`                          |
| `GET /api/parser/stats`            | `GET /api/items/stats`                    |
| `GET /api/parser/health`           | `GET /api/health`                         |
| `GET /api/parser/health/mongodb`   | `GET /api/health/mongodb`                 |
| `GET /api/parser/scheduler/info`   | `GET /api/health/scheduler`               |
