- Web UI: login, dashboard, links CRUD, collections, API keys, admin pages - Query engine: AND/OR/XOR with field filters, tag search, preview endpoint - Session management: token expiry detection, 401 interceptor, expiry banner - Links search: tags included, multi-word AND, query mode with set operations - Collections: static/dynamic, query builder with preview, public tree view - Save as Collection: convert search results (static) or query (dynamic) - Dashboard stats: resilient loading with allSettled pattern - Login page: redesigned with public collections tree view - Bug fix: query executor None fields crash (notes/description/url/title) - E2E tests: 20 Playwright tests covering all critical user flows - All 104 tests passing (84 unit/integration + 20 E2E)
91 lines
2.2 KiB
Python
91 lines
2.2 KiB
Python
"""
|
|
LinkSyncServer - Main Application
|
|
"""
|
|
|
|
import os
|
|
import time
|
|
from fastapi import FastAPI, Request
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.staticfiles import StaticFiles
|
|
from fastapi.templating import Jinja2Templates
|
|
from fastapi.responses import RedirectResponse
|
|
from contextlib import asynccontextmanager
|
|
|
|
from api.routes import router as api_router
|
|
from config.settings import settings
|
|
from models.base import Base, get_engine
|
|
|
|
|
|
BUILD_ID = str(int(time.time()))
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
engine = get_engine()
|
|
Base.metadata.create_all(engine)
|
|
yield
|
|
|
|
|
|
app = FastAPI(
|
|
title="LinkSyncServer",
|
|
description="Self-hosted bookmark server with collections",
|
|
version="1.0.0",
|
|
lifespan=lifespan,
|
|
)
|
|
|
|
cors_origins = [o.strip() for o in settings.CORS_ORIGINS.split(",") if o.strip()]
|
|
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=cors_origins,
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
app.include_router(api_router)
|
|
|
|
app.mount("/static", StaticFiles(directory="static"), name="static")
|
|
|
|
templates = Jinja2Templates(directory="templates")
|
|
|
|
|
|
@app.get("/health")
|
|
def health():
|
|
return {"status": "ok"}
|
|
|
|
|
|
@app.get("/")
|
|
def index():
|
|
return RedirectResponse(url="/login")
|
|
|
|
|
|
@app.get("/login")
|
|
def login_page(request: Request):
|
|
return templates.TemplateResponse("login.html", {"request": request, "build_id": BUILD_ID})
|
|
|
|
|
|
@app.get("/dashboard")
|
|
def dashboard(request: Request):
|
|
return templates.TemplateResponse("dashboard.html", {"request": request, "build_id": BUILD_ID})
|
|
|
|
|
|
@app.get("/links")
|
|
def links_page(request: Request):
|
|
return templates.TemplateResponse("links.html", {"request": request, "build_id": BUILD_ID})
|
|
|
|
|
|
@app.get("/collections")
|
|
def collections_page(request: Request):
|
|
return templates.TemplateResponse("collections.html", {"request": request, "build_id": BUILD_ID})
|
|
|
|
|
|
@app.get("/api-keys")
|
|
def apikeys_page(request: Request):
|
|
return templates.TemplateResponse("apikeys.html", {"request": request, "build_id": BUILD_ID})
|
|
|
|
|
|
@app.get("/admin")
|
|
def admin_page(request: Request):
|
|
return templates.TemplateResponse("admin.html", {"request": request, "build_id": BUILD_ID})
|