diff --git a/app/main.py b/app/main.py index dfa81a9..b643e77 100644 --- a/app/main.py +++ b/app/main.py @@ -6,7 +6,7 @@ from urllib.parse import urlparse from fastapi import Depends, FastAPI, Form, Request from fastapi.responses import HTMLResponse, RedirectResponse, Response from fastapi.templating import Jinja2Templates -from sqlalchemy import func, select +from sqlalchemy import func, select, text from sqlalchemy.orm import Session from starlette.middleware.sessions import SessionMiddleware @@ -17,6 +17,16 @@ from .security import hash_password, verify_password Base.metadata.create_all(bind=engine) +# Einfache Spalten-Migration fΓΌr bestehende SQLite-Datenbanken (kein Alembic +# im Projekt): create_all legt nur neue Tabellen an, keine neuen Spalten. +if config.DATABASE_URL.startswith("sqlite"): + with engine.begin() as _conn: + _cols = {row[1] for row in _conn.execute(text("PRAGMA table_info(links)"))} + if "needs_review" not in _cols: + _conn.execute( + text("ALTER TABLE links ADD COLUMN needs_review BOOLEAN DEFAULT 0") + ) + app = FastAPI(title="LinkVault") app.add_middleware(SessionMiddleware, secret_key=config.SECRET_KEY, max_age=60 * 60 * 24 * 14) @@ -83,9 +93,15 @@ def facets(db: Session, user: User) -> dict: .group_by(Link.manufacturer) .order_by(func.count().desc()) ).all() + review_count = db.scalar( + select(func.count()) + .select_from(Link) + .where(Link.user_id == user.id, Link.needs_review == True) # noqa: E712 + ) return { "categories": [(name, count) for name, count in cat_rows], "manufacturers": [(name, count) for name, count in man_rows], + "review_count": review_count or 0, } @@ -119,12 +135,15 @@ def query_links( manufacturer: str = "", semantic: bool = False, sort: str = DEFAULT_SORT, + review_only: bool = False, ) -> list[Link]: stmt = select(Link).where(Link.user_id == user.id) if category: stmt = stmt.where(Link.category == category) if manufacturer: stmt = stmt.where(Link.manufacturer == manufacturer) + if review_only: + stmt = stmt.where(Link.needs_review == True) # noqa: E712 links = list(db.scalars(stmt.order_by(Link.created_at.desc())).all()) q = (q or "").strip() @@ -231,6 +250,7 @@ def index( manufacturer: str = "", semantic: str = "", sort: str = DEFAULT_SORT, + review: str = "", db: Session = Depends(get_db), ): user = current_user(request, db) @@ -240,7 +260,10 @@ def index( if sort not in SORT_OPTIONS: sort = DEFAULT_SORT is_semantic = semantic in ("1", "on", "true") - links = query_links(db, user, q, category, manufacturer, is_semantic, sort) + review_only = review in ("1", "on", "true") + links = query_links( + db, user, q, category, manufacturer, is_semantic, sort, review_only + ) return templates.TemplateResponse( "index.html", { @@ -255,6 +278,7 @@ def index( "ai_enabled": config.AI_ENABLED, "sort": sort, "sort_options": SORT_OPTIONS, + "review_only": review_only, }, ) @@ -267,6 +291,7 @@ def search_links( manufacturer: str = "", semantic: str = "", sort: str = DEFAULT_SORT, + review: str = "", db: Session = Depends(get_db), ): user = current_user(request, db) @@ -276,7 +301,10 @@ def search_links( if sort not in SORT_OPTIONS: sort = DEFAULT_SORT is_semantic = semantic in ("1", "on", "true") - links = query_links(db, user, q, category, manufacturer, is_semantic, sort) + review_only = review in ("1", "on", "true") + links = query_links( + db, user, q, category, manufacturer, is_semantic, sort, review_only + ) return templates.TemplateResponse( "_links_list.html", {"request": request, "links": links} ) @@ -347,6 +375,26 @@ def delete_link( return Response(status_code=200) +@app.post("/links/{link_id}/toggle-review", response_class=HTMLResponse) +def toggle_review( + link_id: int, + request: Request, + db: Session = Depends(get_db), +): + user = current_user(request, db) + if not user: + return Response(status_code=401, headers={"HX-Redirect": "/login"}) + link = _get_owned_link(db, user, link_id) + if not link: + return Response(status_code=404) + link.needs_review = not link.needs_review + db.commit() + db.refresh(link) + return templates.TemplateResponse( + "_link_card.html", {"request": request, "link": link} + ) + + def _get_owned_link(db: Session, user: User, link_id: int) -> Link | None: link = db.get(Link, link_id) if link and link.user_id == user.id: diff --git a/app/models.py b/app/models.py index c3d6d53..2dbdce2 100644 --- a/app/models.py +++ b/app/models.py @@ -37,6 +37,7 @@ class Link(Base): tags: Mapped[str] = mapped_column(Text, default="[]") # JSON-Liste embedding: Mapped[str] = mapped_column(Text, default="") # JSON-Liste (Vektor) status: Mapped[str] = mapped_column(String(20), default="ok") + needs_review: Mapped[bool] = mapped_column(default=False, index=True) created_at: Mapped[datetime] = mapped_column(DateTime, default=utcnow, index=True) user: Mapped["User"] = relationship(back_populates="links") diff --git a/app/templates/_link_card.html b/app/templates/_link_card.html index 2311ddf..f6ab1e0 100644 --- a/app/templates/_link_card.html +++ b/app/templates/_link_card.html @@ -1,4 +1,4 @@ -