diff --git a/server/main.py b/server/main.py index 3981435..5d5c895 100644 --- a/server/main.py +++ b/server/main.py @@ -53,6 +53,9 @@ SITE_DIR = Path(__file__).parent / "site" # SkinRestorer skins (shared with TG bot) SKINS_DIR = Path("/root/python/site/skins") +# Minecraft server status (same host the TG bot pings) +MC_STATUS_HOST = os.environ.get("MC_STATUS_HOST", "mc.zernmc.ru") + # Mirror configuration LAUNCHER_MIRRORS = { "main": "https://api.zern.cc", # primary @@ -966,6 +969,11 @@ for sub in ("css", "js", "img"): mount_dir.mkdir(parents=True, exist_ok=True) app.mount(f"/{sub}", StaticFiles(directory=mount_dir), name=f"site-{sub}") +# Server highlights media (screenshots / clips for the landing carousel) +HIGHLIGHTS_DIR = SITE_DIR / "highlights" +HIGHLIGHTS_DIR.mkdir(parents=True, exist_ok=True) +app.mount("/highlights", StaticFiles(directory=HIGHLIGHTS_DIR), name="site-highlights") + # Monkey patch to catch invalid HTTP requests original_data_received = HttpToolsProtocol.data_received @@ -1054,6 +1062,44 @@ async def get_skin(filename: str, request: Request): raise HTTPException(status_code=500, detail="Ошибка чтения скина") +@app.get("/api/status") +async def api_server_status(): + """Live Minecraft server status via mcstatus ping (same host as TG bot)""" + try: + from mcstatus import JavaServer + server = JavaServer.lookup(f"{MC_STATUS_HOST}") + status = await server.async_status() + + players_list = [] + try: + query = await server.async_query() + if query.players.names: + players_list = query.players.names + elif query.players.online > 0 and query.players.sample: + players_list = [p.name for p in query.players.sample] + except Exception: + if status.players.sample: + players_list = [p.name for p in status.players.sample] + + version = getattr(status.version, "name", None) or "unknown" + motd = status.description + + return { + "online": True, + "players_online": status.players.online, + "players_max": status.players.max, + "players_list": ", ".join(players_list) if players_list else "никого нет", + "players": players_list, + "motd": motd, + "version": version if isinstance(version, str) else str(version), + "updated": datetime.now().strftime("%H:%M:%S") + } + except asyncio.TimeoutError: + return {"online": False, "error": "Таймаут подключения к серверу", "updated": datetime.now().strftime("%H:%M:%S")} + except Exception as e: + return {"online": False, "error": f"{type(e).__name__}: {e}", "updated": datetime.now().strftime("%H:%M:%S")} + + @app.get("/health") async def health(): """Health check endpoint""" diff --git a/server/middleware.py b/server/middleware.py index 7564c3f..5c71298 100644 --- a/server/middleware.py +++ b/server/middleware.py @@ -159,7 +159,7 @@ class LoggingMiddleware(BaseHTTPMiddleware): # Skip rate limiting for pack file downloads (direct high-speed serving) if path.startswith("/pack/") and "/file/" in path: is_file_download = True - elif path.startswith(("/skin/", "/css/", "/js/", "/img/")): + elif path.startswith(("/skin/", "/css/", "/js/", "/img/", "/highlights/")): # SkinRestorer skins + landing site assets must not be rate-limited is_file_download = True else: diff --git a/server/site/css/style.css b/server/site/css/style.css index cab39f1..d934e63 100644 --- a/server/site/css/style.css +++ b/server/site/css/style.css @@ -346,6 +346,209 @@ ul { list-style: none; } font-weight: 300; } +/* ---------- Server status ---------- */ + +.server { + background: + radial-gradient(800px 400px at 15% 10%, rgba(233, 69, 96, 0.08), transparent 60%), + radial-gradient(800px 400px at 85% 90%, rgba(233, 69, 96, 0.05), transparent 60%); +} + +.server-stats { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 20px; +} + +.server-card { + background: rgba(22, 22, 31, 0.85); + backdrop-filter: blur(16px); + border: 1px solid var(--border); + border-radius: 16px; + padding: 30px 28px; + transition: transform .25s ease, border-color .25s ease; +} + +.server-card:hover { transform: translateY(-4px); border-color: rgba(233, 69, 96, 0.35); } + +.server-card-label { + font-size: 12px; + font-weight: 600; + letter-spacing: 2px; + text-transform: uppercase; + color: var(--text-secondary); + margin-bottom: 14px; +} + +.server-card-value { + font-size: 26px; + font-weight: 800; + line-height: 1.1; + display: flex; + align-items: center; + gap: 10px; +} + +.server-card-sub { font-size: 13px; color: var(--text-secondary); margin-top: 10px; } + +.server-season { color: var(--text-secondary); font-size: 18px; font-weight: 600; } + +.status-dot { + width: 12px; + height: 12px; + border-radius: 50%; + flex: none; + display: inline-block; +} + +.status-dot--online { background: #22c55e; box-shadow: 0 0 16px rgba(34, 197, 94, 0.8); animation: pulse 2s infinite; } +.status-dot--offline { background: #ef4444; box-shadow: 0 0 16px rgba(239, 68, 68, 0.8); } +.status-dot--loading { background: var(--text-secondary); animation: pulse 1.2s infinite; } + +.status-text--online { color: #22c55e; } +.status-text--offline { color: #ef4444; } + +/* ---------- Highlights carousel ---------- */ + +.carousel { + position: relative; + border-radius: 20px; + overflow: hidden; + background: var(--bg-card); + border: 1px solid var(--border); + box-shadow: 0 30px 80px rgba(0, 0, 0, 0.35); +} + +.carousel-track { + display: flex; + transition: transform .6s cubic-bezier(0.4, 0, 0.2, 1); +} + +.carousel-slide { + flex: 0 0 100%; + min-width: 100%; + position: relative; + background: #000; +} + +.carousel-slide img, +.carousel-slide video { + width: 100%; + height: auto; + max-height: 640px; + object-fit: contain; + display: block; +} + +.carousel-caption { + position: absolute; + left: 20px; + bottom: 20px; + background: rgba(7, 7, 10, 0.7); + backdrop-filter: blur(10px); + border: 1px solid var(--border); + color: var(--text); + font-size: 13px; + padding: 8px 14px; + border-radius: 8px; +} + +.carousel-btn { + position: absolute; + top: 50%; + transform: translateY(-50%); + z-index: 5; + width: 52px; + height: 52px; + border-radius: 50%; + border: 1px solid var(--border); + background: rgba(7, 7, 10, 0.6); + backdrop-filter: blur(8px); + color: var(--text); + font-size: 28px; + line-height: 1; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: background .2s ease, transform .15s ease, color .2s ease; +} + +.carousel-btn:hover { background: var(--accent); color: #fff; } + +.carousel-btn--prev { left: 16px; } +.carousel-btn--next { right: 16px; } + +.carousel-btn:active { transform: translateY(-50%) scale(0.92); } + +.carousel-dots { + display: flex; + justify-content: center; + gap: 8px; + padding: 18px 0; + position: absolute; + bottom: 0; + left: 0; + right: 0; + z-index: 5; +} + +.carousel-dot { + width: 9px; + height: 9px; + border-radius: 50%; + background: rgba(255, 255, 255, 0.25); + cursor: pointer; + transition: background .2s ease, transform .2s ease; +} + +.carousel-dot.active { background: var(--accent); transform: scale(1.3); } + +/* ---------- Season theme grid ---------- */ + +.theme { + background: var(--bg-surface); + border-top: 1px solid var(--border); + border-bottom: 1px solid var(--border); +} + +.theme-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 20px; +} + +.theme-card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 16px; + padding: 30px; + text-align: center; + transition: transform .25s ease, border-color .25s ease, background .25s ease; +} + +.theme-card:hover { + transform: translateY(-4px); + border-color: rgba(233, 69, 96, 0.35); + background: #1a1a25; +} + +.theme-tag { + display: inline-block; + font-size: 11px; + font-weight: 600; + letter-spacing: 1px; + text-transform: uppercase; + color: var(--accent); + background: rgba(233, 69, 96, 0.12); + padding: 5px 10px; + border-radius: 6px; + margin-bottom: 16px; +} + +.theme-card h3 { font-size: 20px; font-weight: 700; margin-bottom: 10px; } +.theme-card p { font-size: 14px; color: var(--text-secondary); font-weight: 300; } + /* ---------- Features ---------- */ .features-grid { @@ -595,16 +798,19 @@ ul { list-style: none; } /* ---------- Responsive ---------- */ @media (max-width: 1024px) { - .features-grid, .news-grid { grid-template-columns: repeat(2, 1fr); } + .features-grid, .news-grid, .theme-grid { grid-template-columns: repeat(2, 1fr); } + .server-stats { grid-template-columns: repeat(3, 1fr); } } @media (max-width: 720px) { .nav-links { display: none; } .section { padding: 90px 20px; } - .features-grid, .news-grid { grid-template-columns: 1fr; } + .features-grid, .news-grid, .theme-grid { grid-template-columns: 1fr; } + .server-stats { grid-template-columns: 1fr; } .hero { padding-top: 140px; } .hero-diamond--1 { right: -10%; } .download-card { padding: 26px; } + .carousel-btn { width: 42px; height: 42px; font-size: 22px; } } @media (prefers-reduced-motion: reduce) { diff --git a/server/site/highlights/1.jpg b/server/site/highlights/1.jpg new file mode 100644 index 0000000..b0b292d Binary files /dev/null and b/server/site/highlights/1.jpg differ diff --git a/server/site/highlights/10.jpg b/server/site/highlights/10.jpg new file mode 100644 index 0000000..73595c7 Binary files /dev/null and b/server/site/highlights/10.jpg differ diff --git a/server/site/highlights/11.jpg b/server/site/highlights/11.jpg new file mode 100644 index 0000000..1ca9672 Binary files /dev/null and b/server/site/highlights/11.jpg differ diff --git a/server/site/highlights/12.jpg b/server/site/highlights/12.jpg new file mode 100644 index 0000000..3aad86b Binary files /dev/null and b/server/site/highlights/12.jpg differ diff --git a/server/site/highlights/13.jpg b/server/site/highlights/13.jpg new file mode 100644 index 0000000..a3c62d6 Binary files /dev/null and b/server/site/highlights/13.jpg differ diff --git a/server/site/highlights/14.jpg b/server/site/highlights/14.jpg new file mode 100644 index 0000000..c764d98 Binary files /dev/null and b/server/site/highlights/14.jpg differ diff --git a/server/site/highlights/15.jpg b/server/site/highlights/15.jpg new file mode 100644 index 0000000..2e06f09 Binary files /dev/null and b/server/site/highlights/15.jpg differ diff --git a/server/site/highlights/2.mp4 b/server/site/highlights/2.mp4 new file mode 100644 index 0000000..b66fad2 Binary files /dev/null and b/server/site/highlights/2.mp4 differ diff --git a/server/site/highlights/3.jpg b/server/site/highlights/3.jpg new file mode 100644 index 0000000..23b1c54 Binary files /dev/null and b/server/site/highlights/3.jpg differ diff --git a/server/site/highlights/4.mp4 b/server/site/highlights/4.mp4 new file mode 100644 index 0000000..16d250f Binary files /dev/null and b/server/site/highlights/4.mp4 differ diff --git a/server/site/highlights/5.jpg b/server/site/highlights/5.jpg new file mode 100644 index 0000000..fb53fbb Binary files /dev/null and b/server/site/highlights/5.jpg differ diff --git a/server/site/highlights/6.jpg b/server/site/highlights/6.jpg new file mode 100644 index 0000000..2c78d34 Binary files /dev/null and b/server/site/highlights/6.jpg differ diff --git a/server/site/highlights/7.jpg b/server/site/highlights/7.jpg new file mode 100644 index 0000000..c565ae7 Binary files /dev/null and b/server/site/highlights/7.jpg differ diff --git a/server/site/highlights/8.jpg b/server/site/highlights/8.jpg new file mode 100644 index 0000000..38cc2a7 Binary files /dev/null and b/server/site/highlights/8.jpg differ diff --git a/server/site/highlights/9.jpg b/server/site/highlights/9.jpg new file mode 100644 index 0000000..d533f40 Binary files /dev/null and b/server/site/highlights/9.jpg differ diff --git a/server/site/index.html b/server/site/index.html index ec7e840..edabaa9 100644 --- a/server/site/index.html +++ b/server/site/index.html @@ -20,6 +20,9 @@ Zern