Files
wiki/tasks/apps-portal/docs/TZ.md
2026-04-12 21:55:33 +03:00

6.2 KiB
Raw Blame History

ТЗ: Портал приложений (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.

Алгоритм:

  1. Хэш от name → два цвета для градиента
  2. Градиентный фон (линейный, 135°)
  3. По центру — emoji из поля icon (масштабируется через Pillow, если поддерживается) или первая буква name
  4. Сохранить в 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)