Add mobile interface and add handle metrics

This commit is contained in:
2026-01-26 18:34:12 +03:00
parent dd05e7f5c7
commit 920aaaa424
7 changed files with 82 additions and 34 deletions
+7 -1
View File
@@ -7,6 +7,7 @@ import asyncio
from fastapi import FastAPI, APIRouter, Request, Response
from fastapi.middleware.cors import CORSMiddleware
from prometheus_fastapi_instrumentator import Instrumentator
from app.core.redis import close_redis, init_redis
from app.users.router import router as user_router
@@ -51,7 +52,6 @@ async def get_file(filename: str):
if filename.endswith(".png"):
content_type = "image/png"
# Возвращаем файл напрямую из памяти
return Response(
content=file_data,
media_type=content_type
@@ -61,6 +61,12 @@ app = FastAPI(
description="## Backend messenger aether",
lifespan=lifespan
)
Instrumentator().instrument(app).expose(
app,
endpoint="/api/v1/p2qNT2Cz/SGQmQ==",
include_in_schema=False
)
app.include_router(api_router)
+34 -1
View File
@@ -1681,6 +1681,39 @@ files = [
dev = ["pre-commit", "tox"]
testing = ["coverage", "pytest", "pytest-benchmark"]
[[package]]
name = "prometheus-client"
version = "0.24.1"
description = "Python client for the Prometheus monitoring system."
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "prometheus_client-0.24.1-py3-none-any.whl", hash = "sha256:150db128af71a5c2482b36e588fc8a6b95e498750da4b17065947c16070f4055"},
{file = "prometheus_client-0.24.1.tar.gz", hash = "sha256:7e0ced7fbbd40f7b84962d5d2ab6f17ef88a72504dcf7c0b40737b43b2a461f9"},
]
[package.extras]
aiohttp = ["aiohttp"]
django = ["django"]
twisted = ["twisted"]
[[package]]
name = "prometheus-fastapi-instrumentator"
version = "7.1.0"
description = "Instrument your FastAPI app with Prometheus metrics"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "prometheus_fastapi_instrumentator-7.1.0-py3-none-any.whl", hash = "sha256:978130f3c0bb7b8ebcc90d35516a6fe13e02d2eb358c8f83887cdef7020c31e9"},
{file = "prometheus_fastapi_instrumentator-7.1.0.tar.gz", hash = "sha256:be7cd61eeea4e5912aeccb4261c6631b3f227d8924542d79eaf5af3f439cbe5e"},
]
[package.dependencies]
prometheus-client = ">=0.8.0,<1.0.0"
starlette = ">=0.30.0,<1.0.0"
[[package]]
name = "prompt-toolkit"
version = "3.0.52"
@@ -3841,4 +3874,4 @@ propcache = ">=0.2.1"
[metadata]
lock-version = "2.1"
python-versions = ">=3.13"
content-hash = "0bc3ba3c9bfed0b76ac6aa34c14882ea972d982a889c657d5c39d5c317df1775"
content-hash = "7e4b0bc421fae38a72c65c92dad367b672b4cb7b6179059eac78773689af43db"
+2 -1
View File
@@ -22,7 +22,8 @@ dependencies = [
"pytest (>=9.0.2,<10.0.0)",
"aiobotocore (>=3.1.0,<4.0.0)",
"types-aiobotocore[essential] (>=3.1.0,<4.0.0)",
"websockets (>=16.0,<17.0)"
"websockets (>=16.0,<17.0)",
"prometheus-fastapi-instrumentator (>=7.1.0,<8.0.0)"
]
+6 -6
View File
@@ -8,7 +8,7 @@ export default function AuthPage() {
const [isLogin, setIsLogin] = useState(true);
return (
<div className="min-h-screen flex items-center justify-center p-4 relative" style={{ backgroundColor: '#F5F5F1' }}>
<div className="min-h-screen flex items-center justify-center p-3 md:p-4 relative" style={{ backgroundColor: '#F5F5F1' }}>
{/* Subtle texture background */}
<div className="absolute inset-0 opacity-30 pointer-events-none"
style={{
@@ -23,19 +23,19 @@ export default function AuthPage() {
animate={{ scale: 1, opacity: 1 }}
transition={{ duration: 0.3 }}
>
<div className="bg-card-white rounded-[32px] shadow-soft px-10 py-12">
<div className="bg-card-white rounded-[24px] md:rounded-[32px] shadow-soft px-6 md:px-10 py-8 md:py-12">
{/* Logo */}
<div className="text-center mb-8">
<div className="auth-logo w-[100px] h-[100px] mx-auto mb-8 flex items-center justify-center">
<div className="text-center mb-6 md:mb-8">
<div className="auth-logo w-[80px] h-[80px] md:w-[100px] md:h-[100px] mx-auto mb-6 md:mb-8 flex items-center justify-center">
<img src={miniLogo} alt="Aether Logo" className="w-full h-full object-contain" />
</div>
<div className="font-lora text-accent-olive text-lg tracking-[2px] mb-6">
<div className="font-lora text-accent-olive text-base md:text-lg tracking-[2px] mb-4 md:mb-6">
AETHER
</div>
<AnimatePresence mode="wait">
<motion.h1
key={isLogin ? 'login' : 'register'}
className="font-lora font-semibold text-[28px] text-text-main mb-2"
className="font-lora font-semibold text-2xl md:text-[28px] text-text-main mb-2"
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 10 }}
+17 -13
View File
@@ -454,10 +454,12 @@ export default function ChatPage() {
return (
<div className="min-h-screen h-screen flex" style={{ backgroundColor: 'var(--bg-primary)' }}>
{/* Sidebar */}
<div className="w-80 flex flex-col shadow-soft" style={{ backgroundColor: 'var(--bg-card)' }}>
<div className={`w-full md:w-80 flex flex-col shadow-soft ${
selectedChat ? 'hidden md:flex' : 'flex'
}`} style={{ backgroundColor: 'var(--bg-card)' }}>
{/* Header */}
<div className="p-6 border-b" style={{ borderColor: 'var(--border-color)' }}>
<h1 className="text-3xl font-lora font-semibold text-center tracking-wider" style={{ color: 'var(--accent-primary)' }}>
<div className="p-4 md:p-6 border-b" style={{ borderColor: 'var(--border-color)' }}>
<h1 className="text-2xl md:text-3xl font-lora font-semibold text-center tracking-wider" style={{ color: 'var(--accent-primary)' }}>
AETHER
</h1>
</div>
@@ -470,11 +472,11 @@ export default function ChatPage() {
)}
{/* User Profile Section */}
<div className="p-6 border-b" style={{ borderColor: 'var(--border-color)' }}>
<div className="flex items-center gap-4">
<div className="p-4 md:p-6 border-b" style={{ borderColor: 'var(--border-color)' }}>
<div className="flex items-center gap-3 md:gap-4">
{/* Avatar */}
<div
className="w-14 h-14 flex items-center justify-center flex-shrink-0 cursor-pointer hover:opacity-90 transition"
className="w-12 h-12 md:w-14 md:h-14 flex items-center justify-center flex-shrink-0 cursor-pointer hover:opacity-90 transition"
style={{
backgroundImage: user?.avatar_url ? `url(${user.avatar_url})` : undefined,
backgroundSize: 'cover',
@@ -654,11 +656,13 @@ export default function ChatPage() {
</div>
{/* Main Chat Area */}
<div className="flex-1 flex flex-col relative">
<div className={`flex-1 flex flex-col relative ${
selectedChat ? 'flex' : 'hidden md:flex'
}`}>
{selectedChat ? (
<>
{/* Chat Header */}
<div className="p-4 border-b flex items-center gap-4" style={{
<div className="p-3 md:p-4 border-b flex items-center gap-2 md:gap-4" style={{
backgroundColor: 'var(--bg-card)',
borderColor: 'var(--border-color)'
}}>
@@ -974,7 +978,7 @@ export default function ChatPage() {
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.8, y: 20 }}
onClick={() => scrollToBottom(true)}
className="absolute bottom-24 right-8 p-3 rounded-full shadow-xl hover:scale-110 transition-transform"
className="absolute bottom-20 md:bottom-24 right-4 md:right-8 p-2.5 md:p-3 rounded-full shadow-xl hover:scale-110 transition-transform"
style={{
backgroundColor: 'var(--accent-primary)',
color: 'white',
@@ -1002,11 +1006,11 @@ export default function ChatPage() {
</AnimatePresence>
{/* Message Input */}
<div className="p-4 border-t" style={{
<div className="p-3 md:p-4 border-t" style={{
backgroundColor: 'var(--bg-card)',
borderColor: 'var(--border-color)'
}}>
<div className="flex gap-3 max-w-4xl mx-auto">
<div className="flex gap-2 md:gap-3 max-w-4xl mx-auto">
<input
ref={messageInputRef}
type="text"
@@ -1015,7 +1019,7 @@ export default function ChatPage() {
onKeyPress={handleKeyPress}
placeholder="Написать сообщение..."
disabled={sendingMessage}
className="flex-1 px-4 py-3 rounded-2xl font-inter text-sm outline-none transition"
className="flex-1 px-3 md:px-4 py-2.5 md:py-3 rounded-2xl font-inter text-sm outline-none transition"
style={{
backgroundColor: 'var(--bg-input)',
color: 'var(--text-primary)',
@@ -1028,7 +1032,7 @@ export default function ChatPage() {
whileTap={{ scale: 0.95 }}
onClick={handleSendMessage}
disabled={!messageText.trim() || sendingMessage}
className="px-6 py-3 rounded-2xl font-inter font-semibold flex items-center gap-2 transition hover:opacity-90 disabled:opacity-50 disabled:cursor-not-allowed"
className="px-4 md:px-6 py-2.5 md:py-3 rounded-2xl font-inter font-semibold flex items-center gap-2 transition hover:opacity-90 disabled:opacity-50 disabled:cursor-not-allowed"
style={{ backgroundColor: 'var(--accent-primary)', color: 'white' }}
>
{sendingMessage ? (
+11 -11
View File
@@ -152,16 +152,16 @@ export default function ProfilePage() {
return (
<div className="min-h-screen" style={{ backgroundColor: 'var(--bg-primary)' }}>
<div className="max-w-4xl mx-auto p-6">
<div className="max-w-4xl mx-auto p-4 md:p-6">
{/* Header */}
<div className="mb-6 flex items-center justify-between">
<div className="mb-4 md:mb-6 flex items-center justify-between">
<button
onClick={() => navigate('/chat')}
className="flex items-center gap-2 font-inter font-medium hover:opacity-70 transition"
style={{ color: 'var(--accent-primary)' }}
>
<ArrowLeft size={20} />
Назад к чатам
<span className="hidden sm:inline">Назад к чатам</span>
</button>
<div className="flex items-center gap-3">
<button
@@ -192,14 +192,14 @@ export default function ProfilePage() {
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="rounded-3xl shadow-soft p-8"
className="rounded-2xl md:rounded-3xl shadow-soft p-4 md:p-8"
style={{ backgroundColor: 'var(--bg-card)' }}
>
{/* Avatar Section */}
<div className="flex flex-col items-center mb-8">
<div className="flex flex-col items-center mb-6 md:mb-8">
<div className="relative">
<div
className="w-32 h-32 flex items-center justify-center overflow-hidden"
className="w-24 h-24 md:w-32 md:h-32 flex items-center justify-center overflow-hidden"
style={{
backgroundImage: user.avatar_url ? `url(${user.avatar_url})` : undefined,
backgroundSize: 'cover',
@@ -214,10 +214,10 @@ export default function ProfilePage() {
<label
htmlFor="avatar-upload"
className="absolute bottom-0 right-0 p-2 rounded-full cursor-pointer hover:opacity-80 transition"
className="absolute bottom-0 right-0 p-1.5 md:p-2 rounded-full cursor-pointer hover:opacity-80 transition"
style={{ backgroundColor: 'var(--accent-primary)' }}
>
<Camera size={20} className="text-white" />
<Camera size={18} className="text-white md:w-5 md:h-5" />
<input
id="avatar-upload"
type="file"
@@ -232,16 +232,16 @@ export default function ProfilePage() {
<button
type="button"
onClick={handleDeleteAvatar}
className="absolute bottom-0 left-0 p-2 rounded-full hover:opacity-80 transition"
className="absolute bottom-0 left-0 p-1.5 md:p-2 rounded-full hover:opacity-80 transition"
style={{ backgroundColor: 'var(--error-color)' }}
disabled={isLoading}
>
<Trash2 size={20} className="text-white" />
<Trash2 size={18} className="text-white md:w-5 md:h-5" />
</button>
)}
</div>
<h1 className="mt-4 text-2xl font-lora font-semibold" style={{ color: 'var(--text-primary)' }}>
<h1 className="mt-4 text-xl md:text-2xl font-lora font-semibold" style={{ color: 'var(--text-primary)' }}>
{user.username}
</h1>
<p className="font-inter text-sm" style={{ color: 'var(--text-secondary)' }}>
+5 -1
View File
@@ -32,7 +32,11 @@ export const userService = {
},
updateProfile: async (data: UserUpdate): Promise<User> => {
const response = await apiClient.put('/users/me', data);
// Filter out empty strings to avoid validation errors
const filteredData = Object.fromEntries(
Object.entries(data).filter(([_, value]) => value !== '')
);
const response = await apiClient.put('/users/me', filteredData);
return response.data;
},