6.2 KiB
ТЗ: Портал приложений (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
[
{
"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.
Алгоритм:
- Хэш от
name→ два цвета для градиента - Градиентный фон (линейный, 135°)
- По центру — emoji из поля
icon(масштабируется через Pillow, если поддерживается) или первая букваname - Сохранить в
static/avatars/{id}.png
Примеры градиентов:
- «Карта шума» → синий → тёмно-синий
- «Snowbike Поиск» → зелёный → тёмно-зелёный
- Разные названия → разные цвета (детерминированно)
server.py
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)
<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 (добавить в основной конфиг)
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)