mirror of
https://github.com/lorsanstand/Aether.git
synced 2026-06-19 12:05:16 +03:00
Merge pull request #13 from lorsanstand/dev
Add mobile interface and add handle metrics
This commit is contained in:
+7
-1
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Generated
+34
-1
@@ -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"
|
||||||
|
|||||||
@@ -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)"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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 }}
|
||||||
|
|||||||
@@ -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 ? (
|
||||||
|
|||||||
@@ -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)' }}>
|
||||||
|
|||||||
@@ -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;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user