Files
DavidSaylor fe4cbc3537 feat: add web UI, query engine, session management, and 20 E2E tests
- 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)
2026-05-22 07:46:53 -05:00

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})