185 lines
6.2 KiB
Markdown
185 lines
6.2 KiB
Markdown
# ТЗ: Портал приложений (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)
|