# ТЗ: Портал приложений (apps.mva154.duckdns.org)

## Общее описание

Лендинг-портал с карточками веб-приложений. Светлая тема, автогенерация аватарок, JSON-конфиг.

**URL:** `apps.mva154.duckdns.org`
**Стек:** Flask (порт 5560) + HTML/CSS/JS (один файл) + Pillow (аватарки)
**Бизнес-требования:** `docs/BRD.md`

---

## Файлы

```
tasks/apps-portal/
├── docs/
│   └── BRD.md — бизнес-требования
├── config/
│   └── apps.json — конфиг приложений
├── static/
│   └── avatars/ — сгенерированные аватарки (PNG 200×200)
├── templates/
│   └── index.html — главная страница
├── server.py — Flask сервер
└── requirements.txt — flask, pillow
```

---

## Конфиг: config/apps.json

```json
[
  {
    "id": "noisemap",
    "name": "Карта шума",
    "description": "Карта шумового загрязнения от авиации",
    "icon": "🛩️",
    "url": "https://openclaw.mva154.duckdns.org/noisemap/",
    "enabled": true,
    "order": 1
  },
  {
    "id": "snowbike-rag",
    "name": "Snowbike Поиск",
    "description": "Семантический поиск по 155K сообщений сноубайков",
    "icon": "🏔️",
    "url": "https://openclaw.mva154.duckdns.org/snowbike-rag/",
    "enabled": true,
    "order": 2
  }
]
```

---

## Аватарки: автогенерация

При старте Flask проверяет `static/avatars/`. Для каждого приложения из конфига без файла аватарки — генерирует PNG 200×200.

**Алгоритм:**
1. Хэш от `name` → два цвета для градиента
2. Градиентный фон (линейный, 135°)
3. По центру — emoji из поля `icon` (масштабируется через Pillow, если поддерживается) или первая буква `name`
4. Сохранить в `static/avatars/{id}.png`

**Примеры градиентов:**
- «Карта шума» → синий → тёмно-синий
- «Snowbike Поиск» → зелёный → тёмно-зелёный
- Разные названия → разные цвета (детерминированно)

---

## server.py

```python
from flask import Flask, render_template, send_from_directory
import json
from pathlib import Path

app = Flask(__name__)
CONFIG_FILE = Path(__file__).parent / 'config' / 'apps.json'

def load_apps():
    with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
        apps = json.load(f)
    return sorted([a for a in apps if a.get('enabled', True)], key=lambda x: x.get('order', 99))

@app.route('/')
def index():
    apps = load_apps()
    return render_template('index.html', apps=apps)

@app.route('/api/apps')
def api_apps():
    return load_apps()

@app.route('/static/avatars/<path:filename>')
def avatar(filename):
    return send_from_directory('static/avatars', filename)

if __name__ == '__main__':
    generate_avatars()  # автогенерация при старте
    app.run(host='0.0.0.0', port=5560)
```

---

## index.html

### Структура

```
┌──────────────────────────────────────┐
│  Заголовок: «Мои приложения»         │
│  Подзаголовок: «N активных»          │
│                                      │
│  ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│  │ аватар  │ │ аватар  │ │ аватар  │ │
│  │ Название│ │ Название│ │ Название│ │
│  │ Опис.   │ │ Опис.   │ │ Опис.   │ │
│  └─────────┘ └─────────┘ └─────────┘ │
│                                      │
└──────────────────────────────────────┘
```

### Дизайн
• Светлая тема: фон #F8FAFC, карточки #FFFFFF
• Шрифт: Inter (Google Fonts CDN)
• Tailwind CSS через CDN
• Карточка: 80×80 аватарка по центру, название, описание
• Hover: border синий + lift-тень + scale 1.02
• Адаптивно: 4 → 2 → 1 колонка

### Зависимости (CDN)
```html
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
```

---

## Nginx (добавить в основной конфиг)

```nginx
server {
    server_name apps.mva154.duckdns.org;

    location / {
        proxy_pass http://172.19.0.2:5560/;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    listen 443 ssl;
    ssl_certificate /etc/letsencrypt/live/mva154.duckdns.org/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/mva154.duckdns.org/privkey.pem;
}
```

---

## Критерии приёмки

- [ ] `http://localhost:5560/` — портал с карточками
- [ ] Клик по карточке — переход на URL приложения
- [ ] Аватарки сгенерированы в `static/avatars/`
- [ ] Светлая тема, шрифт Inter
- [ ] Адаптивно на мобильном
- [ ] Добавил приложение в `apps.json` — портал показывает его с аватаркой
- [ ] GET `/api/apps` — JSON-список приложений

---

## Важно

• Всё в `tasks/apps-portal/`
• HTML — один файл (inline CSS + JS)
• Аватарки — Pillow, без внешних API
• Порт 5560 (не пересекается с 5555, 5556, 5557)
