Merge pull request #13 from lorsanstand/dev

Add mobile interface and add handle metrics
This commit is contained in:
Станислав
2026-01-26 18:37:13 +03:00
committed by GitHub
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 import FastAPI, APIRouter, Request, Response
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from prometheus_fastapi_instrumentator import Instrumentator
from app.core.redis import close_redis, init_redis from app.core.redis import close_redis, init_redis
from app.users.router import router as user_router from app.users.router import router as user_router
@@ -51,7 +52,6 @@ async def get_file(filename: str):
if filename.endswith(".png"): if filename.endswith(".png"):
content_type = "image/png" content_type = "image/png"
# Возвращаем файл напрямую из памяти
return Response( return Response(
content=file_data, content=file_data,
media_type=content_type media_type=content_type
@@ -61,6 +61,12 @@ app = FastAPI(
description="## Backend messenger aether", description="## Backend messenger aether",
lifespan=lifespan lifespan=lifespan
) )
Instrumentator().instrument(app).expose(
app,
endpoint="/api/v1/p2qNT2Cz/SGQmQ==",
include_in_schema=False
)
app.include_router(api_router) app.include_router(api_router)
+34 -1
View File
@@ -1681,6 +1681,39 @@ files = [
dev = ["pre-commit", "tox"] dev = ["pre-commit", "tox"]
testing = ["coverage", "pytest", "pytest-benchmark"] 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]] [[package]]
name = "prompt-toolkit" name = "prompt-toolkit"
version = "3.0.52" version = "3.0.52"
@@ -3841,4 +3874,4 @@ propcache = ">=0.2.1"
[metadata] [metadata]
lock-version = "2.1" lock-version = "2.1"
python-versions = ">=3.13" 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)", "pytest (>=9.0.2,<10.0.0)",
"aiobotocore (>=3.1.0,<4.0.0)", "aiobotocore (>=3.1.0,<4.0.0)",
"types-aiobotocore[essential] (>=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); const [isLogin, setIsLogin] = useState(true);
return ( 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 */} {/* Subtle texture background */}
<div className="absolute inset-0 opacity-30 pointer-events-none" <div className="absolute inset-0 opacity-30 pointer-events-none"
style={{ style={{
@@ -23,19 +23,19 @@ export default function AuthPage() {
animate={{ scale: 1, opacity: 1 }} animate={{ scale: 1, opacity: 1 }}
transition={{ duration: 0.3 }} 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 */} {/* Logo */}
<div className="text-center mb-8"> <div className="text-center mb-6 md:mb-8">
<div className="auth-logo w-[100px] h-[100px] mx-auto mb-8 flex items-center justify-center"> <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" /> <img src={miniLogo} alt="Aether Logo" className="w-full h-full object-contain" />
</div> </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 AETHER
</div> </div>
<AnimatePresence mode="wait"> <AnimatePresence mode="wait">
<motion.h1 <motion.h1
key={isLogin ? 'login' : 'register'} 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 }} initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 10 }} exit={{ opacity: 0, y: 10 }}
+17 -13
View File
@@ -454,10 +454,12 @@ export default function ChatPage() {
return ( return (
<div className="min-h-screen h-screen flex" style={{ backgroundColor: 'var(--bg-primary)' }}> <div className="min-h-screen h-screen flex" style={{ backgroundColor: 'var(--bg-primary)' }}>
{/* Sidebar */} {/* 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 */} {/* Header */}
<div className="p-6 border-b" style={{ borderColor: 'var(--border-color)' }}> <div className="p-4 md: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)' }}> <h1 className="text-2xl md:text-3xl font-lora font-semibold text-center tracking-wider" style={{ color: 'var(--accent-primary)' }}>
AETHER AETHER
</h1> </h1>
</div> </div>
@@ -470,11 +472,11 @@ export default function ChatPage() {
)} )}
{/* User Profile Section */} {/* User Profile Section */}
<div className="p-6 border-b" style={{ borderColor: 'var(--border-color)' }}> <div className="p-4 md:p-6 border-b" style={{ borderColor: 'var(--border-color)' }}>
<div className="flex items-center gap-4"> <div className="flex items-center gap-3 md:gap-4">
{/* Avatar */} {/* Avatar */}
<div <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={{ style={{
backgroundImage: user?.avatar_url ? `url(${user.avatar_url})` : undefined, backgroundImage: user?.avatar_url ? `url(${user.avatar_url})` : undefined,
backgroundSize: 'cover', backgroundSize: 'cover',
@@ -654,11 +656,13 @@ export default function ChatPage() {
</div> </div>
{/* Main Chat Area */} {/* 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 ? ( {selectedChat ? (
<> <>
{/* Chat Header */} {/* 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)', backgroundColor: 'var(--bg-card)',
borderColor: 'var(--border-color)' borderColor: 'var(--border-color)'
}}> }}>
@@ -974,7 +978,7 @@ export default function ChatPage() {
animate={{ opacity: 1, scale: 1, y: 0 }} animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.8, y: 20 }} exit={{ opacity: 0, scale: 0.8, y: 20 }}
onClick={() => scrollToBottom(true)} 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={{ style={{
backgroundColor: 'var(--accent-primary)', backgroundColor: 'var(--accent-primary)',
color: 'white', color: 'white',
@@ -1002,11 +1006,11 @@ export default function ChatPage() {
</AnimatePresence> </AnimatePresence>
{/* Message Input */} {/* Message Input */}
<div className="p-4 border-t" style={{ <div className="p-3 md:p-4 border-t" style={{
backgroundColor: 'var(--bg-card)', backgroundColor: 'var(--bg-card)',
borderColor: 'var(--border-color)' 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 <input
ref={messageInputRef} ref={messageInputRef}
type="text" type="text"
@@ -1015,7 +1019,7 @@ export default function ChatPage() {
onKeyPress={handleKeyPress} onKeyPress={handleKeyPress}
placeholder="Написать сообщение..." placeholder="Написать сообщение..."
disabled={sendingMessage} 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={{ style={{
backgroundColor: 'var(--bg-input)', backgroundColor: 'var(--bg-input)',
color: 'var(--text-primary)', color: 'var(--text-primary)',
@@ -1028,7 +1032,7 @@ export default function ChatPage() {
whileTap={{ scale: 0.95 }} whileTap={{ scale: 0.95 }}
onClick={handleSendMessage} onClick={handleSendMessage}
disabled={!messageText.trim() || sendingMessage} 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' }} style={{ backgroundColor: 'var(--accent-primary)', color: 'white' }}
> >
{sendingMessage ? ( {sendingMessage ? (
+11 -11
View File
@@ -152,16 +152,16 @@ export default function ProfilePage() {
return ( return (
<div className="min-h-screen" style={{ backgroundColor: 'var(--bg-primary)' }}> <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 */} {/* Header */}
<div className="mb-6 flex items-center justify-between"> <div className="mb-4 md:mb-6 flex items-center justify-between">
<button <button
onClick={() => navigate('/chat')} onClick={() => navigate('/chat')}
className="flex items-center gap-2 font-inter font-medium hover:opacity-70 transition" className="flex items-center gap-2 font-inter font-medium hover:opacity-70 transition"
style={{ color: 'var(--accent-primary)' }} style={{ color: 'var(--accent-primary)' }}
> >
<ArrowLeft size={20} /> <ArrowLeft size={20} />
Назад к чатам <span className="hidden sm:inline">Назад к чатам</span>
</button> </button>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<button <button
@@ -192,14 +192,14 @@ export default function ProfilePage() {
<motion.div <motion.div
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} 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)' }} style={{ backgroundColor: 'var(--bg-card)' }}
> >
{/* Avatar Section */} {/* 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="relative">
<div <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={{ style={{
backgroundImage: user.avatar_url ? `url(${user.avatar_url})` : undefined, backgroundImage: user.avatar_url ? `url(${user.avatar_url})` : undefined,
backgroundSize: 'cover', backgroundSize: 'cover',
@@ -214,10 +214,10 @@ export default function ProfilePage() {
<label <label
htmlFor="avatar-upload" 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)' }} style={{ backgroundColor: 'var(--accent-primary)' }}
> >
<Camera size={20} className="text-white" /> <Camera size={18} className="text-white md:w-5 md:h-5" />
<input <input
id="avatar-upload" id="avatar-upload"
type="file" type="file"
@@ -232,16 +232,16 @@ export default function ProfilePage() {
<button <button
type="button" type="button"
onClick={handleDeleteAvatar} 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)' }} style={{ backgroundColor: 'var(--error-color)' }}
disabled={isLoading} disabled={isLoading}
> >
<Trash2 size={20} className="text-white" /> <Trash2 size={18} className="text-white md:w-5 md:h-5" />
</button> </button>
)} )}
</div> </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} {user.username}
</h1> </h1>
<p className="font-inter text-sm" style={{ color: 'var(--text-secondary)' }}> <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> => { 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; return response.data;
}, },