mirror of
https://github.com/snailyp/gemini-balance.git
synced 2026-09-07 16:56:36 +08:00
style(database,static): 优化代码格式并本地化静态资源
- 重新组织 database/services.py 的导入语句,按照标准顺序排列 - 统一代码格式,包括函数参数对齐和尾随逗号 - 优化 delete_all_error_logs 函数,移除不必要的计数查询以提高性能 - 添加本地字体文件 fonts.css,包含 Inter 字体的多种字重和语言支持 - 本地化 Tailwind CSS 脚本,减少外部依赖 - 更新 base.html 模板以使用本地静态资源
This commit is contained in:
+69
-58
@@ -1,12 +1,15 @@
|
|||||||
"""
|
"""
|
||||||
数据库服务模块
|
数据库服务模块
|
||||||
"""
|
"""
|
||||||
from typing import List, Optional, Dict, Any, Union
|
|
||||||
from datetime import datetime, timezone
|
|
||||||
from sqlalchemy import func, desc, asc, select, insert, update, delete
|
|
||||||
import json
|
import json
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from typing import Any, Dict, List, Optional, Union
|
||||||
|
|
||||||
|
from sqlalchemy import asc, delete, desc, func, insert, select, update
|
||||||
|
|
||||||
from app.database.connection import database
|
from app.database.connection import database
|
||||||
from app.database.models import Settings, ErrorLog, RequestLog, FileRecord, FileState
|
from app.database.models import ErrorLog, FileRecord, FileState, RequestLog, Settings
|
||||||
from app.log.logger import get_database_logger
|
from app.log.logger import get_database_logger
|
||||||
from app.utils.helpers import redact_key_for_logging
|
from app.utils.helpers import redact_key_for_logging
|
||||||
|
|
||||||
@@ -48,7 +51,9 @@ async def get_setting(key: str) -> Optional[Dict[str, Any]]:
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
async def update_setting(key: str, value: str, description: Optional[str] = None) -> bool:
|
async def update_setting(
|
||||||
|
key: str, value: str, description: Optional[str] = None
|
||||||
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
更新设置
|
更新设置
|
||||||
|
|
||||||
@@ -72,7 +77,7 @@ async def update_setting(key: str, value: str, description: Optional[str] = None
|
|||||||
.values(
|
.values(
|
||||||
value=value,
|
value=value,
|
||||||
description=description if description else setting["description"],
|
description=description if description else setting["description"],
|
||||||
updated_at=datetime.now()
|
updated_at=datetime.now(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
await database.execute(query)
|
await database.execute(query)
|
||||||
@@ -80,15 +85,12 @@ async def update_setting(key: str, value: str, description: Optional[str] = None
|
|||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
# 插入设置
|
# 插入设置
|
||||||
query = (
|
query = insert(Settings).values(
|
||||||
insert(Settings)
|
|
||||||
.values(
|
|
||||||
key=key,
|
key=key,
|
||||||
value=value,
|
value=value,
|
||||||
description=description,
|
description=description,
|
||||||
created_at=datetime.now(),
|
created_at=datetime.now(),
|
||||||
updated_at=datetime.now()
|
updated_at=datetime.now(),
|
||||||
)
|
|
||||||
)
|
)
|
||||||
await database.execute(query)
|
await database.execute(query)
|
||||||
logger.info(f"Inserted setting: {key}")
|
logger.info(f"Inserted setting: {key}")
|
||||||
@@ -104,7 +106,7 @@ async def add_error_log(
|
|||||||
error_type: Optional[str] = None,
|
error_type: Optional[str] = None,
|
||||||
error_log: Optional[str] = None,
|
error_log: Optional[str] = None,
|
||||||
error_code: Optional[int] = None,
|
error_code: Optional[int] = None,
|
||||||
request_msg: Optional[Union[Dict[str, Any], str]] = None
|
request_msg: Optional[Union[Dict[str, Any], str]] = None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
添加错误日志
|
添加错误日志
|
||||||
@@ -131,17 +133,14 @@ async def add_error_log(
|
|||||||
request_msg_json = None
|
request_msg_json = None
|
||||||
|
|
||||||
# 插入错误日志
|
# 插入错误日志
|
||||||
query = (
|
query = insert(ErrorLog).values(
|
||||||
insert(ErrorLog)
|
|
||||||
.values(
|
|
||||||
gemini_key=gemini_key,
|
gemini_key=gemini_key,
|
||||||
error_type=error_type,
|
error_type=error_type,
|
||||||
error_log=error_log,
|
error_log=error_log,
|
||||||
model_name=model_name,
|
model_name=model_name,
|
||||||
error_code=error_code,
|
error_code=error_code,
|
||||||
request_msg=request_msg_json,
|
request_msg=request_msg_json,
|
||||||
request_time=datetime.now()
|
request_time=datetime.now(),
|
||||||
)
|
|
||||||
)
|
)
|
||||||
await database.execute(query)
|
await database.execute(query)
|
||||||
logger.info(f"Added error log for key: {redact_key_for_logging(gemini_key)}")
|
logger.info(f"Added error log for key: {redact_key_for_logging(gemini_key)}")
|
||||||
@@ -159,8 +158,8 @@ async def get_error_logs(
|
|||||||
error_code_search: Optional[str] = None,
|
error_code_search: Optional[str] = None,
|
||||||
start_date: Optional[datetime] = None,
|
start_date: Optional[datetime] = None,
|
||||||
end_date: Optional[datetime] = None,
|
end_date: Optional[datetime] = None,
|
||||||
sort_by: str = 'id',
|
sort_by: str = "id",
|
||||||
sort_order: str = 'desc'
|
sort_order: str = "desc",
|
||||||
) -> List[Dict[str, Any]]:
|
) -> List[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
获取错误日志,支持搜索、日期过滤和排序
|
获取错误日志,支持搜索、日期过滤和排序
|
||||||
@@ -187,15 +186,15 @@ async def get_error_logs(
|
|||||||
ErrorLog.error_type,
|
ErrorLog.error_type,
|
||||||
ErrorLog.error_log,
|
ErrorLog.error_log,
|
||||||
ErrorLog.error_code,
|
ErrorLog.error_code,
|
||||||
ErrorLog.request_time
|
ErrorLog.request_time,
|
||||||
)
|
)
|
||||||
|
|
||||||
if key_search:
|
if key_search:
|
||||||
query = query.where(ErrorLog.gemini_key.ilike(f"%{key_search}%"))
|
query = query.where(ErrorLog.gemini_key.ilike(f"%{key_search}%"))
|
||||||
if error_search:
|
if error_search:
|
||||||
query = query.where(
|
query = query.where(
|
||||||
(ErrorLog.error_type.ilike(f"%{error_search}%")) |
|
(ErrorLog.error_type.ilike(f"%{error_search}%"))
|
||||||
(ErrorLog.error_log.ilike(f"%{error_search}%"))
|
| (ErrorLog.error_log.ilike(f"%{error_search}%"))
|
||||||
)
|
)
|
||||||
if start_date:
|
if start_date:
|
||||||
query = query.where(ErrorLog.request_time >= start_date)
|
query = query.where(ErrorLog.request_time >= start_date)
|
||||||
@@ -206,10 +205,12 @@ async def get_error_logs(
|
|||||||
error_code_int = int(error_code_search)
|
error_code_int = int(error_code_search)
|
||||||
query = query.where(ErrorLog.error_code == error_code_int)
|
query = query.where(ErrorLog.error_code == error_code_int)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
logger.warning(f"Invalid format for error_code_search: '{error_code_search}'. Expected an integer. Skipping error code filter.")
|
logger.warning(
|
||||||
|
f"Invalid format for error_code_search: '{error_code_search}'. Expected an integer. Skipping error code filter."
|
||||||
|
)
|
||||||
|
|
||||||
sort_column = getattr(ErrorLog, sort_by, ErrorLog.id)
|
sort_column = getattr(ErrorLog, sort_by, ErrorLog.id)
|
||||||
if sort_order.lower() == 'asc':
|
if sort_order.lower() == "asc":
|
||||||
query = query.order_by(asc(sort_column))
|
query = query.order_by(asc(sort_column))
|
||||||
else:
|
else:
|
||||||
query = query.order_by(desc(sort_column))
|
query = query.order_by(desc(sort_column))
|
||||||
@@ -228,7 +229,7 @@ async def get_error_logs_count(
|
|||||||
error_search: Optional[str] = None,
|
error_search: Optional[str] = None,
|
||||||
error_code_search: Optional[str] = None,
|
error_code_search: Optional[str] = None,
|
||||||
start_date: Optional[datetime] = None,
|
start_date: Optional[datetime] = None,
|
||||||
end_date: Optional[datetime] = None
|
end_date: Optional[datetime] = None,
|
||||||
) -> int:
|
) -> int:
|
||||||
"""
|
"""
|
||||||
获取符合条件的错误日志总数
|
获取符合条件的错误日志总数
|
||||||
@@ -250,8 +251,8 @@ async def get_error_logs_count(
|
|||||||
query = query.where(ErrorLog.gemini_key.ilike(f"%{key_search}%"))
|
query = query.where(ErrorLog.gemini_key.ilike(f"%{key_search}%"))
|
||||||
if error_search:
|
if error_search:
|
||||||
query = query.where(
|
query = query.where(
|
||||||
(ErrorLog.error_type.ilike(f"%{error_search}%")) |
|
(ErrorLog.error_type.ilike(f"%{error_search}%"))
|
||||||
(ErrorLog.error_log.ilike(f"%{error_search}%"))
|
| (ErrorLog.error_log.ilike(f"%{error_search}%"))
|
||||||
)
|
)
|
||||||
if start_date:
|
if start_date:
|
||||||
query = query.where(ErrorLog.request_time >= start_date)
|
query = query.where(ErrorLog.request_time >= start_date)
|
||||||
@@ -262,8 +263,9 @@ async def get_error_logs_count(
|
|||||||
error_code_int = int(error_code_search)
|
error_code_int = int(error_code_search)
|
||||||
query = query.where(ErrorLog.error_code == error_code_int)
|
query = query.where(ErrorLog.error_code == error_code_int)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
logger.warning(f"Invalid format for error_code_search in count: '{error_code_search}'. Expected an integer. Skipping error code filter.")
|
logger.warning(
|
||||||
|
f"Invalid format for error_code_search in count: '{error_code_search}'. Expected an integer. Skipping error code filter."
|
||||||
|
)
|
||||||
|
|
||||||
count_result = await database.fetch_one(query)
|
count_result = await database.fetch_one(query)
|
||||||
return count_result[0] if count_result else 0
|
return count_result[0] if count_result else 0
|
||||||
@@ -289,12 +291,14 @@ async def get_error_log_details(log_id: int) -> Optional[Dict[str, Any]]:
|
|||||||
if result:
|
if result:
|
||||||
# 将 request_msg (JSONB) 转换为字符串以便在 API 中返回
|
# 将 request_msg (JSONB) 转换为字符串以便在 API 中返回
|
||||||
log_dict = dict(result)
|
log_dict = dict(result)
|
||||||
if 'request_msg' in log_dict and log_dict['request_msg'] is not None:
|
if "request_msg" in log_dict and log_dict["request_msg"] is not None:
|
||||||
# 确保即使是 None 或非 JSON 数据也能处理
|
# 确保即使是 None 或非 JSON 数据也能处理
|
||||||
try:
|
try:
|
||||||
log_dict['request_msg'] = json.dumps(log_dict['request_msg'], ensure_ascii=False, indent=2)
|
log_dict["request_msg"] = json.dumps(
|
||||||
|
log_dict["request_msg"], ensure_ascii=False, indent=2
|
||||||
|
)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
log_dict['request_msg'] = str(log_dict['request_msg'])
|
log_dict["request_msg"] = str(log_dict["request_msg"])
|
||||||
return log_dict
|
return log_dict
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
@@ -330,9 +334,12 @@ async def delete_error_logs_by_ids(log_ids: List[int]) -> int:
|
|||||||
return len(log_ids) # 返回尝试删除的数量
|
return len(log_ids) # 返回尝试删除的数量
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# 数据库连接或执行错误
|
# 数据库连接或执行错误
|
||||||
logger.error(f"Error during bulk deletion of error logs {log_ids}: {e}", exc_info=True)
|
logger.error(
|
||||||
|
f"Error during bulk deletion of error logs {log_ids}: {e}", exc_info=True
|
||||||
|
)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
async def delete_error_log_by_id(log_id: int) -> bool:
|
async def delete_error_log_by_id(log_id: int) -> bool:
|
||||||
"""
|
"""
|
||||||
根据 ID 删除单个错误日志 (异步)。
|
根据 ID 删除单个错误日志 (异步)。
|
||||||
@@ -349,7 +356,9 @@ async def delete_error_log_by_id(log_id: int) -> bool:
|
|||||||
exists = await database.fetch_one(check_query)
|
exists = await database.fetch_one(check_query)
|
||||||
|
|
||||||
if not exists:
|
if not exists:
|
||||||
logger.warning(f"Attempted to delete non-existent error log with ID: {log_id}")
|
logger.warning(
|
||||||
|
f"Attempted to delete non-existent error log with ID: {log_id}"
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# 执行删除
|
# 执行删除
|
||||||
@@ -367,23 +376,19 @@ async def delete_all_error_logs() -> int:
|
|||||||
删除所有错误日志条目。
|
删除所有错误日志条目。
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
int: 被删除的错误日志数量。
|
int: 被删除的错误日志数量。如果使用的数据库驱动不支持返回受影响行数,则返回 -1 表示操作成功。
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# 1. 获取删除前的总数
|
# 直接执行删除操作,避免不必要的查询
|
||||||
count_query = select(func.count()).select_from(ErrorLog)
|
|
||||||
total_to_delete = await database.fetch_val(count_query)
|
|
||||||
|
|
||||||
if total_to_delete == 0:
|
|
||||||
logger.info("No error logs found to delete.")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
# 2. 执行删除操作
|
|
||||||
delete_query = delete(ErrorLog)
|
delete_query = delete(ErrorLog)
|
||||||
await database.execute(delete_query)
|
await database.execute(delete_query)
|
||||||
|
|
||||||
logger.info(f"Successfully deleted all {total_to_delete} error logs.")
|
logger.info("Successfully deleted all error logs.")
|
||||||
return total_to_delete
|
|
||||||
|
# 由于 databases 库的 execute 方法不返回受影响的行数,
|
||||||
|
# 返回 -1 表示删除操作成功执行,但具体删除数量未知
|
||||||
|
# 这比先查询再删除的方式更高效
|
||||||
|
return -1
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to delete all error logs: {str(e)}", exc_info=True)
|
logger.error(f"Failed to delete all error logs: {str(e)}", exc_info=True)
|
||||||
raise
|
raise
|
||||||
@@ -396,7 +401,7 @@ async def add_request_log(
|
|||||||
is_success: bool,
|
is_success: bool,
|
||||||
status_code: Optional[int] = None,
|
status_code: Optional[int] = None,
|
||||||
latency_ms: Optional[int] = None,
|
latency_ms: Optional[int] = None,
|
||||||
request_time: Optional[datetime] = None
|
request_time: Optional[datetime] = None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
添加 API 请求日志
|
添加 API 请求日志
|
||||||
@@ -421,7 +426,7 @@ async def add_request_log(
|
|||||||
api_key=api_key,
|
api_key=api_key,
|
||||||
is_success=is_success,
|
is_success=is_success,
|
||||||
status_code=status_code,
|
status_code=status_code,
|
||||||
latency_ms=latency_ms
|
latency_ms=latency_ms,
|
||||||
)
|
)
|
||||||
await database.execute(query)
|
await database.execute(query)
|
||||||
return True
|
return True
|
||||||
@@ -432,6 +437,7 @@ async def add_request_log(
|
|||||||
|
|
||||||
# ==================== 文件记录相关函数 ====================
|
# ==================== 文件记录相关函数 ====================
|
||||||
|
|
||||||
|
|
||||||
async def create_file_record(
|
async def create_file_record(
|
||||||
name: str,
|
name: str,
|
||||||
mime_type: str,
|
mime_type: str,
|
||||||
@@ -445,7 +451,7 @@ async def create_file_record(
|
|||||||
display_name: Optional[str] = None,
|
display_name: Optional[str] = None,
|
||||||
sha256_hash: Optional[str] = None,
|
sha256_hash: Optional[str] = None,
|
||||||
upload_url: Optional[str] = None,
|
upload_url: Optional[str] = None,
|
||||||
user_token: Optional[str] = None
|
user_token: Optional[str] = None,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
创建文件记录
|
创建文件记录
|
||||||
@@ -481,7 +487,7 @@ async def create_file_record(
|
|||||||
uri=uri,
|
uri=uri,
|
||||||
api_key=api_key,
|
api_key=api_key,
|
||||||
upload_url=upload_url,
|
upload_url=upload_url,
|
||||||
user_token=user_token
|
user_token=user_token,
|
||||||
)
|
)
|
||||||
await database.execute(query)
|
await database.execute(query)
|
||||||
|
|
||||||
@@ -511,13 +517,12 @@ async def get_file_record_by_name(name: str) -> Optional[Dict[str, Any]]:
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async def update_file_record_state(
|
async def update_file_record_state(
|
||||||
file_name: str,
|
file_name: str,
|
||||||
state: FileState,
|
state: FileState,
|
||||||
update_time: Optional[datetime] = None,
|
update_time: Optional[datetime] = None,
|
||||||
upload_completed: Optional[datetime] = None,
|
upload_completed: Optional[datetime] = None,
|
||||||
sha256_hash: Optional[str] = None
|
sha256_hash: Optional[str] = None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
更新文件记录状态
|
更新文件记录状态
|
||||||
@@ -559,7 +564,7 @@ async def list_file_records(
|
|||||||
user_token: Optional[str] = None,
|
user_token: Optional[str] = None,
|
||||||
api_key: Optional[str] = None,
|
api_key: Optional[str] = None,
|
||||||
page_size: int = 10,
|
page_size: int = 10,
|
||||||
page_token: Optional[str] = None
|
page_token: Optional[str] = None,
|
||||||
) -> tuple[List[Dict[str, Any]], Optional[str]]:
|
) -> tuple[List[Dict[str, Any]], Optional[str]]:
|
||||||
"""
|
"""
|
||||||
列出文件记录
|
列出文件记录
|
||||||
@@ -574,7 +579,9 @@ async def list_file_records(
|
|||||||
tuple[List[Dict[str, Any]], Optional[str]]: (文件列表, 下一页标记)
|
tuple[List[Dict[str, Any]], Optional[str]]: (文件列表, 下一页标记)
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
logger.debug(f"list_file_records called with page_size={page_size}, page_token={page_token}")
|
logger.debug(
|
||||||
|
f"list_file_records called with page_size={page_size}, page_token={page_token}"
|
||||||
|
)
|
||||||
query = select(FileRecord).where(
|
query = select(FileRecord).where(
|
||||||
FileRecord.expiration_time > datetime.now(timezone.utc)
|
FileRecord.expiration_time > datetime.now(timezone.utc)
|
||||||
)
|
)
|
||||||
@@ -600,7 +607,9 @@ async def list_file_records(
|
|||||||
|
|
||||||
logger.debug(f"Query returned {len(results)} records")
|
logger.debug(f"Query returned {len(results)} records")
|
||||||
if results:
|
if results:
|
||||||
logger.debug(f"First record ID: {results[0]['id']}, Last record ID: {results[-1]['id']}")
|
logger.debug(
|
||||||
|
f"First record ID: {results[0]['id']}, Last record ID: {results[-1]['id']}"
|
||||||
|
)
|
||||||
|
|
||||||
# 处理分页
|
# 处理分页
|
||||||
has_next = len(results) > page_size
|
has_next = len(results) > page_size
|
||||||
@@ -609,7 +618,9 @@ async def list_file_records(
|
|||||||
# 下一页的偏移量是当前偏移量加上本页返回的记录数
|
# 下一页的偏移量是当前偏移量加上本页返回的记录数
|
||||||
next_offset = offset + page_size
|
next_offset = offset + page_size
|
||||||
next_page_token = str(next_offset)
|
next_page_token = str(next_offset)
|
||||||
logger.debug(f"Has next page, offset={offset}, page_size={page_size}, next_page_token={next_page_token}")
|
logger.debug(
|
||||||
|
f"Has next page, offset={offset}, page_size={page_size}, next_page_token={next_page_token}"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
next_page_token = None
|
next_page_token = None
|
||||||
logger.debug(f"No next page, returning {len(results)} results")
|
logger.debug(f"No next page, returning {len(results)} results")
|
||||||
@@ -681,8 +692,8 @@ async def get_file_api_key(name: str) -> Optional[str]:
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
query = select(FileRecord.api_key).where(
|
query = select(FileRecord.api_key).where(
|
||||||
(FileRecord.name == name) &
|
(FileRecord.name == name)
|
||||||
(FileRecord.expiration_time > datetime.now(timezone.utc))
|
& (FileRecord.expiration_time > datetime.now(timezone.utc))
|
||||||
)
|
)
|
||||||
result = await database.fetch_one(query)
|
result = await database.fetch_one(query)
|
||||||
return result["api_key"] if result else None
|
return result["api_key"] if result else None
|
||||||
|
|||||||
@@ -0,0 +1,315 @@
|
|||||||
|
/* cyrillic-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v19/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2JL7SUc.woff2) format('woff2');
|
||||||
|
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||||
|
}
|
||||||
|
/* cyrillic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v19/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa0ZL7SUc.woff2) format('woff2');
|
||||||
|
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||||
|
}
|
||||||
|
/* greek-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v19/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2ZL7SUc.woff2) format('woff2');
|
||||||
|
unicode-range: U+1F00-1FFF;
|
||||||
|
}
|
||||||
|
/* greek */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v19/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1pL7SUc.woff2) format('woff2');
|
||||||
|
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
|
||||||
|
}
|
||||||
|
/* vietnamese */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v19/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2pL7SUc.woff2) format('woff2');
|
||||||
|
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||||
|
}
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v19/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa25L7SUc.woff2) format('woff2');
|
||||||
|
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v19/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
/* cyrillic-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v19/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2JL7SUc.woff2) format('woff2');
|
||||||
|
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||||
|
}
|
||||||
|
/* cyrillic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v19/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa0ZL7SUc.woff2) format('woff2');
|
||||||
|
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||||
|
}
|
||||||
|
/* greek-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v19/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2ZL7SUc.woff2) format('woff2');
|
||||||
|
unicode-range: U+1F00-1FFF;
|
||||||
|
}
|
||||||
|
/* greek */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v19/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1pL7SUc.woff2) format('woff2');
|
||||||
|
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
|
||||||
|
}
|
||||||
|
/* vietnamese */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v19/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2pL7SUc.woff2) format('woff2');
|
||||||
|
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||||
|
}
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v19/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa25L7SUc.woff2) format('woff2');
|
||||||
|
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v19/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
/* cyrillic-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v19/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2JL7SUc.woff2) format('woff2');
|
||||||
|
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||||
|
}
|
||||||
|
/* cyrillic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v19/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa0ZL7SUc.woff2) format('woff2');
|
||||||
|
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||||
|
}
|
||||||
|
/* greek-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v19/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2ZL7SUc.woff2) format('woff2');
|
||||||
|
unicode-range: U+1F00-1FFF;
|
||||||
|
}
|
||||||
|
/* greek */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v19/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1pL7SUc.woff2) format('woff2');
|
||||||
|
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
|
||||||
|
}
|
||||||
|
/* vietnamese */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v19/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2pL7SUc.woff2) format('woff2');
|
||||||
|
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||||
|
}
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v19/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa25L7SUc.woff2) format('woff2');
|
||||||
|
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v19/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
/* cyrillic-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v19/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2JL7SUc.woff2) format('woff2');
|
||||||
|
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||||
|
}
|
||||||
|
/* cyrillic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v19/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa0ZL7SUc.woff2) format('woff2');
|
||||||
|
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||||
|
}
|
||||||
|
/* greek-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v19/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2ZL7SUc.woff2) format('woff2');
|
||||||
|
unicode-range: U+1F00-1FFF;
|
||||||
|
}
|
||||||
|
/* greek */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v19/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1pL7SUc.woff2) format('woff2');
|
||||||
|
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
|
||||||
|
}
|
||||||
|
/* vietnamese */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v19/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2pL7SUc.woff2) format('woff2');
|
||||||
|
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||||
|
}
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v19/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa25L7SUc.woff2) format('woff2');
|
||||||
|
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v19/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
/* cyrillic-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v19/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2JL7SUc.woff2) format('woff2');
|
||||||
|
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||||
|
}
|
||||||
|
/* cyrillic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v19/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa0ZL7SUc.woff2) format('woff2');
|
||||||
|
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||||
|
}
|
||||||
|
/* greek-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v19/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2ZL7SUc.woff2) format('woff2');
|
||||||
|
unicode-range: U+1F00-1FFF;
|
||||||
|
}
|
||||||
|
/* greek */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v19/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1pL7SUc.woff2) format('woff2');
|
||||||
|
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
|
||||||
|
}
|
||||||
|
/* vietnamese */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v19/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2pL7SUc.woff2) format('woff2');
|
||||||
|
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||||
|
}
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v19/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa25L7SUc.woff2) format('woff2');
|
||||||
|
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v19/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -11,14 +11,14 @@
|
|||||||
<meta name="apple-mobile-web-app-title" content="GBalance" />
|
<meta name="apple-mobile-web-app-title" content="GBalance" />
|
||||||
<link rel="icon" href="/static/icons/icon-192x192.png" />
|
<link rel="icon" href="/static/icons/icon-192x192.png" />
|
||||||
<link
|
<link
|
||||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap"
|
href="/static/css/fonts.css"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
/>
|
/>
|
||||||
<link
|
<link
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
|
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
|
||||||
/>
|
/>
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="/static/js/tailwindcss.js"></script>
|
||||||
<script>
|
<script>
|
||||||
tailwind.config = {
|
tailwind.config = {
|
||||||
theme: {
|
theme: {
|
||||||
|
|||||||
Reference in New Issue
Block a user