Database backup
Browse files- Dockerfile +3 -0
- README.md +7 -3
- backend/Makefile +2 -2
- backend/src/app.py +6 -79
- backend/src/app_factory.py +158 -0
- backend/src/constants.py +10 -2
- backend/src/database.py +0 -31
- frontend/.env +0 -1
- frontend/index.html +2 -2
Dockerfile
CHANGED
@@ -77,6 +77,9 @@ RUN chown -R appuser:appuser /app/tmp_data
|
|
77 |
# Set frontend path for FastAPI
|
78 |
ENV FRONTEND_PATH=/app/frontend/dist
|
79 |
|
|
|
|
|
|
|
80 |
# Switch to non-root user
|
81 |
USER appuser
|
82 |
|
|
|
77 |
# Set frontend path for FastAPI
|
78 |
ENV FRONTEND_PATH=/app/frontend/dist
|
79 |
|
80 |
+
# Set HF_HUB_DISABLE_EXPERIMENTAL_WARNING to avoid warnings
|
81 |
+
ENV HF_HUB_DISABLE_EXPERIMENTAL_WARNING=1
|
82 |
+
|
83 |
# Switch to non-root user
|
84 |
USER appuser
|
85 |
|
README.md
CHANGED
@@ -21,6 +21,10 @@ Spaces are very well suited to vibe-code demos using Cursor, Lovable, Claude Cod
|
|
21 |
|
22 |
The backend (Python + FastAPI) and the frontend (TS + React + Tailwind) are located respectively in the `backend/` and `frontend/` folder.
|
23 |
|
|
|
|
|
|
|
|
|
24 |
### Backend
|
25 |
|
26 |
To run the backend, you'll need `uv` and Python3.12 installed.
|
@@ -67,8 +71,8 @@ Configuration should be automatically done if you clone the on the Hugging Face
|
|
67 |
If you want to run it on your own, you can use Docker:
|
68 |
|
69 |
```bash
|
70 |
-
docker
|
71 |
-
docker
|
72 |
```
|
73 |
|
74 |
-
|
|
|
21 |
|
22 |
The backend (Python + FastAPI) and the frontend (TS + React + Tailwind) are located respectively in the `backend/` and `frontend/` folder.
|
23 |
|
24 |
+
### Prerequisite
|
25 |
+
|
26 |
+
To run this app locally, you MUST have `HF_TOKEN` set as environment variable with a valid token. You can generate a new one from your [user settings](https://huggingface.co/settings/tokens). This token is necessary to fake the OAuth workflow. If you click on "Sign in with Huggingface" in your dev environment, you will be authenticated as yourself and your local `HF_TOKEN` will be used. This is a mock to allow local testing while avoiding more complex configuration.
|
27 |
+
|
28 |
### Backend
|
29 |
|
30 |
To run the backend, you'll need `uv` and Python3.12 installed.
|
|
|
71 |
If you want to run it on your own, you can use Docker:
|
72 |
|
73 |
```bash
|
74 |
+
make docker-build
|
75 |
+
make docker-run
|
76 |
```
|
77 |
|
78 |
+
When running in Docker, the app runs in production mode without hot-reloading.
|
backend/Makefile
CHANGED
@@ -1,7 +1,7 @@
|
|
1 |
.PHONY: quality style
|
2 |
|
3 |
dev:
|
4 |
-
uv run uvicorn src.app:app --reload
|
5 |
|
6 |
quality:
|
7 |
ruff check src
|
@@ -13,4 +13,4 @@ style:
|
|
13 |
ruff check --fix src
|
14 |
|
15 |
preview:
|
16 |
-
FRONTEND_PATH=../frontend/dist/ uv run uvicorn src.app:app
|
|
|
1 |
.PHONY: quality style
|
2 |
|
3 |
dev:
|
4 |
+
HF_HUB_DISABLE_EXPERIMENTAL_WARNING=1 uv run uvicorn src.app:app --reload
|
5 |
|
6 |
quality:
|
7 |
ruff check src
|
|
|
13 |
ruff check --fix src
|
14 |
|
15 |
preview:
|
16 |
+
HF_HUB_DISABLE_EXPERIMENTAL_WARNING=1 FRONTEND_PATH=../frontend/dist/ uv run uvicorn src.app:app
|
backend/src/app.py
CHANGED
@@ -1,79 +1,10 @@
|
|
1 |
-
from contextlib import asynccontextmanager
|
2 |
-
|
3 |
-
from fastapi import Depends, FastAPI, HTTPException, Request
|
4 |
-
from fastapi.middleware.cors import CORSMiddleware
|
5 |
-
from fastapi.responses import FileResponse, RedirectResponse
|
6 |
-
from fastapi.staticfiles import StaticFiles
|
7 |
-
from huggingface_hub import OAuthInfo, attach_huggingface_oauth, parse_huggingface_oauth
|
8 |
from sqlmodel import select
|
9 |
|
10 |
-
from . import
|
11 |
-
from .database import get_session, init_db
|
12 |
from .schemas import UserCount
|
13 |
|
14 |
-
|
15 |
-
|
16 |
-
@asynccontextmanager
|
17 |
-
async def lifespan(app: FastAPI):
|
18 |
-
init_db()
|
19 |
-
yield
|
20 |
-
|
21 |
-
|
22 |
-
# FastAPI app
|
23 |
-
app = FastAPI(lifespan=lifespan)
|
24 |
-
|
25 |
-
|
26 |
-
# Set CORS headers
|
27 |
-
app.add_middleware(
|
28 |
-
CORSMiddleware,
|
29 |
-
allow_origins=[
|
30 |
-
# Can't use "*" because frontend doesn't like it with "credentials: true"
|
31 |
-
"http://localhost:5173",
|
32 |
-
"http://0.0.0.0:9481",
|
33 |
-
"http://localhost:9481",
|
34 |
-
"http://127.0.0.1:9481",
|
35 |
-
],
|
36 |
-
allow_credentials=True,
|
37 |
-
allow_methods=["*"],
|
38 |
-
allow_headers=["*"],
|
39 |
-
)
|
40 |
-
|
41 |
-
# Mount frontend from dist directory (if configured)
|
42 |
-
if constants.SERVE_FRONTEND:
|
43 |
-
# Production => Serve frontend from dist directory
|
44 |
-
app.mount(
|
45 |
-
"/assets",
|
46 |
-
StaticFiles(directory=constants.FRONTEND_ASSETS_PATH),
|
47 |
-
name="assets",
|
48 |
-
)
|
49 |
-
|
50 |
-
@app.get("/")
|
51 |
-
async def serve_frontend():
|
52 |
-
return FileResponse(constants.FRONTEND_INDEX_PATH)
|
53 |
-
|
54 |
-
else:
|
55 |
-
# Development => Redirect to dev frontend
|
56 |
-
@app.get("/")
|
57 |
-
async def redirect_to_frontend():
|
58 |
-
return RedirectResponse("http://localhost:5173/")
|
59 |
-
|
60 |
-
|
61 |
-
# Set up Hugging Face OAuth
|
62 |
-
# To get OAuthInfo in an endpoint
|
63 |
-
attach_huggingface_oauth(app)
|
64 |
-
|
65 |
-
|
66 |
-
async def oauth_info_optional(request: Request) -> OAuthInfo | None:
|
67 |
-
return parse_huggingface_oauth(request)
|
68 |
-
|
69 |
-
|
70 |
-
async def oauth_info_required(request: Request) -> OAuthInfo:
|
71 |
-
oauth_info = parse_huggingface_oauth(request)
|
72 |
-
if oauth_info is None:
|
73 |
-
raise HTTPException(
|
74 |
-
status_code=401, detail="Unauthorized. Please Sign in with Hugging Face."
|
75 |
-
)
|
76 |
-
return oauth_info
|
77 |
|
78 |
|
79 |
# Health check endpoint
|
@@ -85,7 +16,7 @@ async def health():
|
|
85 |
|
86 |
# User endpoints
|
87 |
@app.get("/api/user")
|
88 |
-
async def get_user(oauth_info:
|
89 |
"""Get user information."""
|
90 |
return {
|
91 |
"connected": oauth_info is not None,
|
@@ -94,9 +25,7 @@ async def get_user(oauth_info: OAuthInfo | None = Depends(oauth_info_optional)):
|
|
94 |
|
95 |
|
96 |
@app.get("/api/user/count")
|
97 |
-
async def get_user_count(
|
98 |
-
oauth_info: OAuthInfo = Depends(oauth_info_required),
|
99 |
-
) -> UserCount:
|
100 |
"""Get user count."""
|
101 |
with get_session() as session:
|
102 |
statement = select(UserCount).where(UserCount.name == oauth_info.user_info.name)
|
@@ -107,9 +36,7 @@ async def get_user_count(
|
|
107 |
|
108 |
|
109 |
@app.post("/api/user/count/increment")
|
110 |
-
async def increment_user_count(
|
111 |
-
oauth_info: OAuthInfo = Depends(oauth_info_required),
|
112 |
-
) -> UserCount:
|
113 |
"""Increment user count."""
|
114 |
with get_session() as session:
|
115 |
statement = select(UserCount).where(UserCount.name == oauth_info.user_info.name)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
from sqlmodel import select
|
2 |
|
3 |
+
from .app_factory import OptionalOAuth, RequiredOAuth, create_app, get_session
|
|
|
4 |
from .schemas import UserCount
|
5 |
|
6 |
+
# Configure FastAPI app + database
|
7 |
+
app = create_app()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
8 |
|
9 |
|
10 |
# Health check endpoint
|
|
|
16 |
|
17 |
# User endpoints
|
18 |
@app.get("/api/user")
|
19 |
+
async def get_user(oauth_info: OptionalOAuth):
|
20 |
"""Get user information."""
|
21 |
return {
|
22 |
"connected": oauth_info is not None,
|
|
|
25 |
|
26 |
|
27 |
@app.get("/api/user/count")
|
28 |
+
async def get_user_count(oauth_info: RequiredOAuth) -> UserCount:
|
|
|
|
|
29 |
"""Get user count."""
|
30 |
with get_session() as session:
|
31 |
statement = select(UserCount).where(UserCount.name == oauth_info.user_info.name)
|
|
|
36 |
|
37 |
|
38 |
@app.post("/api/user/count/increment")
|
39 |
+
async def increment_user_count(oauth_info: RequiredOAuth) -> UserCount:
|
|
|
|
|
40 |
"""Increment user count."""
|
41 |
with get_session() as session:
|
42 |
statement = select(UserCount).where(UserCount.name == oauth_info.user_info.name)
|
backend/src/app_factory.py
ADDED
@@ -0,0 +1,158 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
from contextlib import asynccontextmanager, contextmanager
|
3 |
+
from typing import Annotated, Generator
|
4 |
+
|
5 |
+
from fastapi import Depends, FastAPI, HTTPException, Request
|
6 |
+
from fastapi.middleware.cors import CORSMiddleware
|
7 |
+
from fastapi.responses import FileResponse, RedirectResponse
|
8 |
+
from fastapi.staticfiles import StaticFiles
|
9 |
+
from huggingface_hub import OAuthInfo, attach_huggingface_oauth, parse_huggingface_oauth
|
10 |
+
from sqlalchemy.engine import Engine
|
11 |
+
from sqlmodel import Session, SQLModel, create_engine
|
12 |
+
|
13 |
+
from . import constants
|
14 |
+
|
15 |
+
_ENGINE_SINGLETON: Engine | None = None
|
16 |
+
|
17 |
+
# OAuth utilities
|
18 |
+
|
19 |
+
|
20 |
+
async def _oauth_info_optional(request: Request) -> OAuthInfo | None:
|
21 |
+
return parse_huggingface_oauth(request)
|
22 |
+
|
23 |
+
|
24 |
+
async def _oauth_info_required(request: Request) -> OAuthInfo:
|
25 |
+
oauth_info = parse_huggingface_oauth(request)
|
26 |
+
if oauth_info is None:
|
27 |
+
raise HTTPException(
|
28 |
+
status_code=401, detail="Unauthorized. Please Sign in with Hugging Face."
|
29 |
+
)
|
30 |
+
return oauth_info
|
31 |
+
|
32 |
+
|
33 |
+
OptionalOAuth = Annotated[OAuthInfo | None, Depends(_oauth_info_optional)]
|
34 |
+
RequiredOAuth = Annotated[OAuthInfo, Depends(_oauth_info_required)]
|
35 |
+
|
36 |
+
|
37 |
+
# Database utilities
|
38 |
+
def _get_engine() -> Engine:
|
39 |
+
"""Get the engine."""
|
40 |
+
global _ENGINE_SINGLETON
|
41 |
+
if _ENGINE_SINGLETON is None:
|
42 |
+
_ENGINE_SINGLETON = create_engine(constants.DATABASE_URL)
|
43 |
+
return _ENGINE_SINGLETON
|
44 |
+
|
45 |
+
|
46 |
+
@contextmanager
|
47 |
+
def get_session() -> Generator[Session, None, None]:
|
48 |
+
"""Get a session from the engine."""
|
49 |
+
engine = _get_engine()
|
50 |
+
with Session(engine) as session:
|
51 |
+
yield session
|
52 |
+
|
53 |
+
|
54 |
+
@asynccontextmanager
|
55 |
+
async def _database_lifespan(app: FastAPI):
|
56 |
+
"""Handle database lifespan.
|
57 |
+
|
58 |
+
1. If backup is enabled enabled,
|
59 |
+
a. Try to load backup from remote dataset. If it fails, delete local database for a fresh start.
|
60 |
+
b. Start back-up scheduler.
|
61 |
+
2. If disabled, create local database file or reuse existing one.
|
62 |
+
3. Initialize database.
|
63 |
+
4. Yield control to FastAPI app.
|
64 |
+
5. Close database + force push backup to remote dataset.
|
65 |
+
"""
|
66 |
+
if constants.BACKUP_DB:
|
67 |
+
from huggingface_hub import CommitScheduler, hf_hub_download
|
68 |
+
|
69 |
+
print("Back-up database is enabled")
|
70 |
+
|
71 |
+
# Try to load backup from remote dataset
|
72 |
+
print("Trying to load backup from remote dataset...")
|
73 |
+
try:
|
74 |
+
hf_hub_download(
|
75 |
+
repo_id=constants.BACKUP_DATASET_ID, # type: ignore[arg-type]
|
76 |
+
repo_type="dataset",
|
77 |
+
filename="database.db",
|
78 |
+
token=constants.HF_TOKEN,
|
79 |
+
local_dir=constants.DATABASE_PATH,
|
80 |
+
force_download=True,
|
81 |
+
)
|
82 |
+
except Exception:
|
83 |
+
# If backup is enabled but no backup is found, delete local database to prevent confusion.
|
84 |
+
print("Couldn't find backup in remote dataset.")
|
85 |
+
print("Deleting local database for a fresh start.")
|
86 |
+
os.remove(constants.DATABASE_FILE)
|
87 |
+
|
88 |
+
# Start back-up scheduler
|
89 |
+
print("Starting back-up scheduler (save every 5 minutes)...")
|
90 |
+
scheduler = CommitScheduler(
|
91 |
+
repo_id=constants.BACKUP_DATASET_ID, # type: ignore[arg-type]
|
92 |
+
folder_path=constants.DATABASE_PATH,
|
93 |
+
allow_patterns="database.db",
|
94 |
+
token=constants.HF_TOKEN,
|
95 |
+
private=True,
|
96 |
+
repo_type="dataset",
|
97 |
+
every=5,
|
98 |
+
)
|
99 |
+
|
100 |
+
engine = _get_engine()
|
101 |
+
SQLModel.metadata.create_all(engine)
|
102 |
+
|
103 |
+
yield
|
104 |
+
|
105 |
+
print("Closing database...")
|
106 |
+
global _ENGINE_SINGLETON
|
107 |
+
if _ENGINE_SINGLETON is not None:
|
108 |
+
_ENGINE_SINGLETON.dispose()
|
109 |
+
_ENGINE_SINGLETON = None
|
110 |
+
|
111 |
+
if constants.BACKUP_DB:
|
112 |
+
print("Pushing backup to remote dataset...")
|
113 |
+
scheduler.push_to_hub()
|
114 |
+
|
115 |
+
|
116 |
+
def create_app() -> FastAPI:
|
117 |
+
# FastAPI app
|
118 |
+
app = FastAPI(lifespan=_database_lifespan)
|
119 |
+
|
120 |
+
# Set CORS headers
|
121 |
+
app.add_middleware(
|
122 |
+
CORSMiddleware,
|
123 |
+
allow_origins=[
|
124 |
+
# Can't use "*" because frontend doesn't like it with "credentials: true"
|
125 |
+
"http://localhost:5173",
|
126 |
+
"http://0.0.0.0:9481",
|
127 |
+
"http://localhost:9481",
|
128 |
+
"http://127.0.0.1:9481",
|
129 |
+
],
|
130 |
+
allow_credentials=True,
|
131 |
+
allow_methods=["*"],
|
132 |
+
allow_headers=["*"],
|
133 |
+
)
|
134 |
+
|
135 |
+
# Mount frontend from dist directory (if configured)
|
136 |
+
if constants.SERVE_FRONTEND:
|
137 |
+
# Production => Serve frontend from dist directory
|
138 |
+
app.mount(
|
139 |
+
"/assets",
|
140 |
+
StaticFiles(directory=constants.FRONTEND_ASSETS_PATH), # type: ignore[invalid-argument-type]
|
141 |
+
name="assets",
|
142 |
+
)
|
143 |
+
|
144 |
+
@app.get("/")
|
145 |
+
async def serve_frontend():
|
146 |
+
return FileResponse(constants.FRONTEND_INDEX_PATH) # type: ignore[invalid-argument-type]
|
147 |
+
|
148 |
+
else:
|
149 |
+
# Development => Redirect to dev frontend
|
150 |
+
@app.get("/")
|
151 |
+
async def redirect_to_frontend():
|
152 |
+
return RedirectResponse("http://localhost:5173/")
|
153 |
+
|
154 |
+
# Set up Hugging Face OAuth
|
155 |
+
# To get OAuthInfo in an endpoint
|
156 |
+
attach_huggingface_oauth(app)
|
157 |
+
|
158 |
+
return app
|
backend/src/constants.py
CHANGED
@@ -13,9 +13,11 @@ DATABASE_URL = f"sqlite:///{DATABASE_FILE}"
|
|
13 |
# In practice: is it running in a production environment?
|
14 |
FRONTEND_PATH = os.getenv("FRONTEND_PATH")
|
15 |
SERVE_FRONTEND = os.getenv("FRONTEND_PATH") is not None
|
16 |
-
FRONTEND_ASSETS_PATH =
|
|
|
|
|
17 |
FRONTEND_INDEX_PATH = (
|
18 |
-
os.path.join(FRONTEND_PATH, "index.html") if
|
19 |
)
|
20 |
|
21 |
|
@@ -28,3 +30,9 @@ if SERVE_FRONTEND and (
|
|
28 |
f"FRONTEND_PATH {FRONTEND_PATH} has not been built correctly. Please build the frontend first by running `pnpm build` from the 'frontend/' directory."
|
29 |
" If you want to run the server in development mode, run `make dev` from the 'backend/' directory and `pnpm dev` from the 'frontend/' directory."
|
30 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
13 |
# In practice: is it running in a production environment?
|
14 |
FRONTEND_PATH = os.getenv("FRONTEND_PATH")
|
15 |
SERVE_FRONTEND = os.getenv("FRONTEND_PATH") is not None
|
16 |
+
FRONTEND_ASSETS_PATH = (
|
17 |
+
os.path.join(FRONTEND_PATH, "assets") if FRONTEND_PATH is not None else None
|
18 |
+
)
|
19 |
FRONTEND_INDEX_PATH = (
|
20 |
+
os.path.join(FRONTEND_PATH, "index.html") if FRONTEND_PATH is not None else None
|
21 |
)
|
22 |
|
23 |
|
|
|
30 |
f"FRONTEND_PATH {FRONTEND_PATH} has not been built correctly. Please build the frontend first by running `pnpm build` from the 'frontend/' directory."
|
31 |
" If you want to run the server in development mode, run `make dev` from the 'backend/' directory and `pnpm dev` from the 'frontend/' directory."
|
32 |
)
|
33 |
+
|
34 |
+
|
35 |
+
# Back-up Hub dataset (optional)
|
36 |
+
BACKUP_DATASET_ID = os.getenv("BACKUP_DATASET_ID")
|
37 |
+
HF_TOKEN = os.getenv("HF_TOKEN")
|
38 |
+
BACKUP_DB = BACKUP_DATASET_ID is not None and HF_TOKEN is not None
|
backend/src/database.py
DELETED
@@ -1,31 +0,0 @@
|
|
1 |
-
from contextlib import contextmanager
|
2 |
-
from typing import Generator
|
3 |
-
|
4 |
-
from sqlalchemy.engine import Engine
|
5 |
-
from sqlmodel import Session, SQLModel, create_engine
|
6 |
-
|
7 |
-
from . import constants
|
8 |
-
|
9 |
-
_ENGINE_SINGLETON: Engine | None = None
|
10 |
-
|
11 |
-
|
12 |
-
def get_engine() -> Engine:
|
13 |
-
"""Get the engine."""
|
14 |
-
global _ENGINE_SINGLETON
|
15 |
-
if _ENGINE_SINGLETON is None:
|
16 |
-
_ENGINE_SINGLETON = create_engine(constants.DATABASE_URL)
|
17 |
-
return _ENGINE_SINGLETON
|
18 |
-
|
19 |
-
|
20 |
-
@contextmanager
|
21 |
-
def get_session() -> Generator[Session, None, None]:
|
22 |
-
"""Get a session from the engine."""
|
23 |
-
engine = get_engine()
|
24 |
-
with Session(engine) as session:
|
25 |
-
yield session
|
26 |
-
|
27 |
-
|
28 |
-
def init_db() -> None:
|
29 |
-
"""Initialize the database."""
|
30 |
-
engine = get_engine()
|
31 |
-
SQLModel.metadata.create_all(engine)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/.env
DELETED
@@ -1 +0,0 @@
|
|
1 |
-
VITE_BACKEND_URL=http://127.0.0.1:8000
|
|
|
|
frontend/index.html
CHANGED
@@ -2,9 +2,9 @@
|
|
2 |
<html lang="en">
|
3 |
<head>
|
4 |
<meta charset="UTF-8" />
|
5 |
-
<link rel="icon" type="image/svg+xml" href="/
|
6 |
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
7 |
-
<title>
|
8 |
</head>
|
9 |
<body>
|
10 |
<div id="root"></div>
|
|
|
2 |
<html lang="en">
|
3 |
<head>
|
4 |
<meta charset="UTF-8" />
|
5 |
+
<link rel="icon" type="image/svg+xml" href="/src/assets/huggingface.svg" />
|
6 |
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
7 |
+
<title>FastAPI + React Space Template</title>
|
8 |
</head>
|
9 |
<body>
|
10 |
<div id="root"></div>
|